codeclimate-services 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.ruby-version +1 -0
  4. data/.travis.yml +13 -0
  5. data/Gemfile +7 -0
  6. data/LICENSE.txt +22 -0
  7. data/README.md +121 -0
  8. data/Rakefile +11 -0
  9. data/bin/bundler +16 -0
  10. data/bin/coderay +16 -0
  11. data/bin/nokogiri +16 -0
  12. data/bin/pry +16 -0
  13. data/bin/rake +16 -0
  14. data/codeclimate-services.gemspec +29 -0
  15. data/config/cacert.pem +4026 -0
  16. data/config/load.rb +4 -0
  17. data/lib/axiom/types/password.rb +7 -0
  18. data/lib/cc/formatters/linked_formatter.rb +60 -0
  19. data/lib/cc/formatters/plain_formatter.rb +36 -0
  20. data/lib/cc/formatters/snapshot_formatter.rb +101 -0
  21. data/lib/cc/formatters/ticket_formatter.rb +27 -0
  22. data/lib/cc/helpers/coverage_helper.rb +25 -0
  23. data/lib/cc/helpers/issue_helper.rb +9 -0
  24. data/lib/cc/helpers/quality_helper.rb +44 -0
  25. data/lib/cc/helpers/vulnerability_helper.rb +31 -0
  26. data/lib/cc/presenters/github_pull_requests_presenter.rb +54 -0
  27. data/lib/cc/service/config.rb +4 -0
  28. data/lib/cc/service/formatter.rb +34 -0
  29. data/lib/cc/service/helper.rb +54 -0
  30. data/lib/cc/service/http.rb +87 -0
  31. data/lib/cc/service/invocation/invocation_chain.rb +15 -0
  32. data/lib/cc/service/invocation/with_error_handling.rb +45 -0
  33. data/lib/cc/service/invocation/with_metrics.rb +37 -0
  34. data/lib/cc/service/invocation/with_retries.rb +17 -0
  35. data/lib/cc/service/invocation/with_return_values.rb +18 -0
  36. data/lib/cc/service/invocation.rb +57 -0
  37. data/lib/cc/service/response_check.rb +42 -0
  38. data/lib/cc/service.rb +127 -0
  39. data/lib/cc/services/asana.rb +90 -0
  40. data/lib/cc/services/campfire.rb +55 -0
  41. data/lib/cc/services/flowdock.rb +61 -0
  42. data/lib/cc/services/github_issues.rb +80 -0
  43. data/lib/cc/services/github_pull_requests.rb +210 -0
  44. data/lib/cc/services/hipchat.rb +57 -0
  45. data/lib/cc/services/jira.rb +93 -0
  46. data/lib/cc/services/lighthouse.rb +79 -0
  47. data/lib/cc/services/pivotal_tracker.rb +78 -0
  48. data/lib/cc/services/slack.rb +124 -0
  49. data/lib/cc/services/version.rb +5 -0
  50. data/lib/cc/services.rb +9 -0
  51. data/pull_request_test.rb +47 -0
  52. data/service_test.rb +86 -0
  53. data/test/asana_test.rb +85 -0
  54. data/test/axiom/types/password_test.rb +22 -0
  55. data/test/campfire_test.rb +144 -0
  56. data/test/fixtures.rb +68 -0
  57. data/test/flowdock_test.rb +148 -0
  58. data/test/formatters/snapshot_formatter_test.rb +47 -0
  59. data/test/github_issues_test.rb +96 -0
  60. data/test/github_pull_requests_test.rb +293 -0
  61. data/test/helper.rb +50 -0
  62. data/test/hipchat_test.rb +130 -0
  63. data/test/invocation_error_handling_test.rb +51 -0
  64. data/test/invocation_return_values_test.rb +21 -0
  65. data/test/invocation_test.rb +167 -0
  66. data/test/jira_test.rb +80 -0
  67. data/test/lighthouse_test.rb +74 -0
  68. data/test/pivotal_tracker_test.rb +73 -0
  69. data/test/presenters/github_pull_requests_presenter_test.rb +49 -0
  70. data/test/service_test.rb +63 -0
  71. data/test/slack_test.rb +222 -0
  72. data/test/support/fake_logger.rb +11 -0
  73. data/test/with_metrics_test.rb +19 -0
  74. metadata +263 -0
data/config/load.rb ADDED
@@ -0,0 +1,4 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require File.expand_path("../../lib/cc/services", __FILE__)
@@ -0,0 +1,7 @@
1
+ class Axiom::Types::Password < Axiom::Types::String
2
+ def self.infer(object)
3
+ if object == Axiom::Types::Password
4
+ self
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,60 @@
1
+ module CC
2
+ module Formatters
3
+ class LinkedFormatter < CC::Service::Formatter
4
+ def format_test
5
+ message = message_prefix
6
+ message << "This is a test of the #{service_title} service hook"
7
+ end
8
+
9
+ def format_coverage
10
+ message = message_prefix
11
+ message << "#{format_link(details_url, "Test coverage")}"
12
+ message << " has #{changed} to #{covered_percent}% (#{delta})"
13
+
14
+ if compare_url
15
+ message << " (#{format_link(compare_url, "Compare")})"
16
+ end
17
+
18
+ message
19
+ end
20
+
21
+ def format_quality
22
+ message = message_prefix
23
+ message << "#{format_link(details_url, constant_name)}"
24
+ message << " has #{changed} from #{previous_rating} to #{rating}"
25
+
26
+ if compare_url
27
+ message << " (#{format_link(compare_url, "Compare")})"
28
+ end
29
+
30
+ message
31
+ end
32
+
33
+ def format_vulnerability
34
+ message = message_prefix
35
+
36
+ if multiple?
37
+ message << "#{vulnerabilities.size} new"
38
+ message << " #{format_link(details_url, warning_type)}"
39
+ message << " issues found"
40
+ else
41
+ message << "New #{format_link(details_url, warning_type)}"
42
+ message << " issue found"
43
+ message << location_info
44
+ end
45
+
46
+ message
47
+ end
48
+
49
+ private
50
+
51
+ def format_link(url, text)
52
+ case options[:link_style]
53
+ when :html then "<a href=\"#{url}\">#{text}</a>"
54
+ when :wiki then "<#{url}|#{text}>"
55
+ else text
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,36 @@
1
+ module CC
2
+ module Formatters
3
+ class PlainFormatter < CC::Service::Formatter
4
+ def format_test
5
+ message = message_prefix
6
+ message << "This is a test of the #{service_title} service hook"
7
+ end
8
+
9
+ def format_coverage
10
+ message = message_prefix
11
+ message << "#{emoji} Test coverage has #{changed}"
12
+ message << " to #{covered_percent}% (#{delta})."
13
+ message << " (#{details_url})"
14
+ end
15
+
16
+ def format_quality
17
+ message = message_prefix
18
+ message << "#{emoji} #{constant_name} has #{changed}"
19
+ message << " from #{previous_rating} to #{rating}."
20
+ message << " (#{details_url})"
21
+ end
22
+
23
+ def format_vulnerability
24
+ message = message_prefix
25
+
26
+ if multiple?
27
+ message << "#{vulnerabilities.size} new #{warning_type} issues found"
28
+ else
29
+ message << "New #{warning_type} issue found" << location_info
30
+ end
31
+
32
+ message << ". Details: #{details_url}"
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,101 @@
1
+ module CC::Formatters
2
+ module SnapshotFormatter
3
+ # Simple Comparator for rating letters.
4
+ class Rating
5
+ include Comparable
6
+
7
+ def initialize(letter)
8
+ @letter = letter
9
+ end
10
+
11
+ def <=>(other)
12
+ other.to_s <=> to_s
13
+ end
14
+
15
+ def hash
16
+ @letter.hash
17
+ end
18
+
19
+ def eql?(other)
20
+ to_s == other.to_s
21
+ end
22
+
23
+ def inspect
24
+ "<Rating:#{to_s}>"
25
+ end
26
+
27
+ def to_s
28
+ @letter.to_s
29
+ end
30
+ end
31
+
32
+ C = Rating.new("C")
33
+ D = Rating.new("D")
34
+
35
+ # SnapshotFormatter::Base takes the quality information from the payload and divides it
36
+ # between alerts and improvements.
37
+ #
38
+ # The information in the payload must be a comparison in time between two quality reports, aka snapshot.
39
+ # This information is in the payload when the service receive a `receive_snapshot` and also
40
+ # when it receives a `receive_test`. In this latest case, the comparison is between today and seven days ago.
41
+ class Base
42
+ attr_reader :alert_constants_payload, :improved_constants_payload, :details_url, :compare_url
43
+
44
+ def initialize(payload)
45
+ new_constants = Array(payload["new_constants"])
46
+ changed_constants = Array(payload["changed_constants"])
47
+
48
+ alert_constants = new_constants.select(&new_constants_selector)
49
+ alert_constants += changed_constants.select(&decreased_constants_selector)
50
+
51
+ improved_constants = changed_constants.select(&improved_constants_selector)
52
+
53
+ data = {
54
+ "from" => { "commit_sha" => payload["previous_commit_sha"] },
55
+ "to" => { "commit_sha" => payload["commit_sha"] }
56
+ }
57
+
58
+ @alert_constants_payload = data.merge("constants" => alert_constants) if alert_constants.any?
59
+ @improved_constants_payload = data.merge("constants" => improved_constants) if improved_constants.any?
60
+ end
61
+
62
+ private
63
+
64
+ def new_constants_selector
65
+ Proc.new { |constant| to_rating(constant) < C }
66
+ end
67
+
68
+ def decreased_constants_selector
69
+ Proc.new { |constant| from_rating(constant) > D && to_rating(constant) < C }
70
+ end
71
+
72
+ def improved_constants_selector
73
+ Proc.new { |constant| from_rating(constant) < C && to_rating(constant) > from_rating(constant) }
74
+ end
75
+
76
+ def to_rating(constant)
77
+ Rating.new(constant["to"]["rating"])
78
+ end
79
+
80
+ def from_rating(constant)
81
+ Rating.new(constant["from"]["rating"])
82
+ end
83
+ end
84
+
85
+ # Override the base snapshot formatter for be more lax grouping information.
86
+ # This is useful to show more information for testing the service.
87
+ class Sample < Base
88
+ def new_constants_selector
89
+ Proc.new { |_| true }
90
+ end
91
+
92
+ def decreased_constants_selector
93
+ Proc.new { |constant| to_rating(constant) < from_rating(constant) }
94
+ end
95
+
96
+ def improved_constants_selector
97
+ Proc.new { |constant| to_rating(constant) > from_rating(constant) }
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,27 @@
1
+ module CC
2
+ module Formatters
3
+ class TicketFormatter < CC::Service::Formatter
4
+
5
+ def format_vulnerability_title
6
+ if multiple?
7
+ "#{vulnerabilities.size} new #{warning_type} issues found"
8
+ else
9
+ "New #{warning_type} issue found" << location_info
10
+ end
11
+ end
12
+
13
+ def format_vulnerability_body
14
+ if multiple?
15
+ "#{vulnerabilities.size} new #{warning_type} issues were found by Code Climate"
16
+ else
17
+ message = "A #{warning_type} vulnerability was found by Code Climate"
18
+ message << location_info
19
+ end
20
+
21
+ message << ".\n\n"
22
+ message << details_url
23
+ end
24
+
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,25 @@
1
+ module CC::Service::CoverageHelper
2
+ def improved?
3
+ covered_percent_delta > 0
4
+ end
5
+
6
+ def delta
7
+ if improved?
8
+ "+#{covered_percent_delta.round(1)}%"
9
+ else
10
+ "#{covered_percent_delta.round(1)}%"
11
+ end
12
+ end
13
+
14
+ def covered_percent
15
+ payload.fetch("covered_percent", 0).round(1)
16
+ end
17
+
18
+ def previous_covered_percent
19
+ payload.fetch("previous_covered_percent", 0).round(1)
20
+ end
21
+
22
+ def covered_percent_delta
23
+ payload.fetch("covered_percent_delta", 0) # pre-rounded
24
+ end
25
+ end
@@ -0,0 +1,9 @@
1
+ module CC::Service::IssueHelper
2
+ def constant_name
3
+ payload["constant_name"]
4
+ end
5
+
6
+ def issue
7
+ payload["issue"]
8
+ end
9
+ end
@@ -0,0 +1,44 @@
1
+ module CC::Service::QualityHelper
2
+ def improved?
3
+ remediation_cost < previous_remediation_cost
4
+ end
5
+
6
+ def constant_name
7
+ payload["constant_name"]
8
+ end
9
+
10
+ def rating
11
+ with_article(payload["rating"])
12
+ end
13
+
14
+ def previous_rating
15
+ with_article(payload["previous_rating"])
16
+ end
17
+
18
+ def remediation_cost
19
+ payload.fetch("remediation_cost", 0)
20
+ end
21
+
22
+ def previous_remediation_cost
23
+ payload.fetch("previous_remediation_cost", 0)
24
+ end
25
+
26
+ def with_article(letter, bold = false)
27
+ letter ||= '?'
28
+
29
+ text = bold ? "*#{letter}*" : letter
30
+ if %w( A F ).include?(letter.to_s)
31
+ "an #{text}"
32
+ else
33
+ "a #{text}"
34
+ end
35
+ end
36
+
37
+ def constant_basename(name)
38
+ if name.include?(".")
39
+ File.basename(name)
40
+ else
41
+ name
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,31 @@
1
+ module CC::Service::VulnerabilityHelper
2
+
3
+ def vulnerability
4
+ vulnerabilities.first || {}
5
+ end
6
+
7
+ def vulnerabilities
8
+ payload.fetch("vulnerabilities", [])
9
+ end
10
+
11
+ def multiple?
12
+ vulnerabilities.size > 1
13
+ end
14
+
15
+ def location_info
16
+ if vulnerability["location"]
17
+ " in #{vulnerability["location"]}"
18
+ else
19
+ ""
20
+ end
21
+ end
22
+
23
+ def warning_type
24
+ if multiple?
25
+ payload["warning_type"]
26
+ else
27
+ vulnerability["warning_type"]
28
+ end
29
+ end
30
+
31
+ end
@@ -0,0 +1,54 @@
1
+ module CC
2
+ class Service
3
+ class GitHubPullRequestsPresenter
4
+ include ActiveSupport::NumberHelper
5
+
6
+ def initialize(payload)
7
+ issue_comparison_counts = payload["issue_comparison_counts"]
8
+
9
+ if issue_comparison_counts
10
+ @fixed_count = issue_comparison_counts["fixed"]
11
+ @new_count = issue_comparison_counts["new"]
12
+ end
13
+ end
14
+
15
+ def success_message
16
+ if both_issue_counts_zero?
17
+ "Code Climate didn't find any new or fixed issues."
18
+ else
19
+ "Code Climate found #{formatted_issue_counts}."
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def both_issue_counts_zero?
26
+ issue_counts.all?(&:zero?)
27
+ end
28
+
29
+ def formatted_fixed_issues
30
+ if @fixed_count > 0
31
+ "#{number_to_delimited(@fixed_count)} fixed #{"issue".pluralize(@fixed_count)}"
32
+ else
33
+ nil
34
+ end
35
+ end
36
+
37
+ def formatted_new_issues
38
+ if @new_count > 0
39
+ "#{number_to_delimited(@new_count)} new #{"issue".pluralize(@new_count)}"
40
+ else
41
+ nil
42
+ end
43
+ end
44
+
45
+ def formatted_issue_counts
46
+ [formatted_new_issues, formatted_fixed_issues].compact.to_sentence
47
+ end
48
+
49
+ def issue_counts
50
+ [@new_count, @fixed_count]
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,4 @@
1
+ class CC::Service::Config
2
+ include Virtus.model
3
+ include ActiveModel::Validations
4
+ end
@@ -0,0 +1,34 @@
1
+ require 'delegate'
2
+
3
+ class CC::Service::Formatter < SimpleDelegator
4
+ attr_reader :options
5
+
6
+ def initialize(service, options = {})
7
+ super(service)
8
+
9
+ @options = {
10
+ prefix: "[Code Climate]",
11
+ prefix_with_repo: true
12
+ }.merge(options)
13
+ end
14
+
15
+ private
16
+
17
+ def service_title
18
+ __getobj__.class.title
19
+ end
20
+
21
+ def message_prefix
22
+ prefix = options.fetch(:prefix, "").to_s
23
+
24
+ if options[:prefix_with_repo]
25
+ prefix << "[#{repo_name}]"
26
+ end
27
+
28
+ if !prefix.empty?
29
+ prefix << " "
30
+ end
31
+
32
+ prefix
33
+ end
34
+ end
@@ -0,0 +1,54 @@
1
+ module CC::Service::Helper
2
+ GREEN_HEX = "#38ae6f"
3
+ RED_HEX = "#ed2f00"
4
+
5
+ def repo_name
6
+ payload["repo_name"]
7
+ end
8
+
9
+ def details_url
10
+ payload["details_url"]
11
+ end
12
+
13
+ def compare_url
14
+ payload["compare_url"]
15
+ end
16
+
17
+ def emoji
18
+ if improved?
19
+ ":sunny:"
20
+ else
21
+ ":umbrella:"
22
+ end
23
+ end
24
+
25
+ def color
26
+ if improved?
27
+ "green"
28
+ else
29
+ "red"
30
+ end
31
+ end
32
+
33
+ def hex_color
34
+ if improved?
35
+ GREEN_HEX
36
+ else
37
+ RED_HEX
38
+ end
39
+ end
40
+
41
+ def changed
42
+ if improved?
43
+ "improved"
44
+ else
45
+ "declined"
46
+ end
47
+ end
48
+
49
+ def improved?
50
+ raise NotImplementedError,
51
+ "Event-specific helpers must define #{__method__}"
52
+ end
53
+
54
+ end
@@ -0,0 +1,87 @@
1
+ require "active_support/concern"
2
+ require "cc/service/response_check"
3
+
4
+ module CC::Service::HTTP
5
+ extend ActiveSupport::Concern
6
+
7
+ module ClassMethods
8
+ def default_http_options
9
+ @@default_http_options ||= {
10
+ adapter: :net_http,
11
+ request: { timeout: 10, open_timeout: 5 },
12
+ ssl: { verify_depth: 5 },
13
+ headers: {}
14
+ }
15
+ end
16
+ end
17
+
18
+ def service_get(url = nil, body = nil, headers = nil, &block)
19
+ raw_get(url, body, headers, &block)
20
+ end
21
+
22
+ def service_post(url, body = nil, headers = nil, &block)
23
+ block ||= lambda{|*args| Hash.new }
24
+ response = raw_post(url, body, headers)
25
+ {
26
+ ok: response.success?,
27
+ params: body.as_json,
28
+ endpoint_url: url,
29
+ status: response.status,
30
+ message: "Success"
31
+ }.merge(block.call(response))
32
+ end
33
+
34
+ def raw_get(url = nil, params = nil, headers = nil)
35
+ http.get do |req|
36
+ req.url(url) if url
37
+ req.params.update(params) if params
38
+ req.headers.update(headers) if headers
39
+ yield req if block_given?
40
+ end
41
+ end
42
+
43
+ def raw_post(url = nil, body = nil, headers = nil)
44
+ block = Proc.new if block_given?
45
+ http_method :post, url, body, headers, &block
46
+ end
47
+
48
+ def http_method(method, url = nil, body = nil, headers = nil)
49
+ block = Proc.new if block_given?
50
+
51
+ http.send(method) do |req|
52
+ req.url(url) if url
53
+ req.headers.update(headers) if headers
54
+ req.body = body if body
55
+ block.call req if block
56
+ end
57
+ end
58
+
59
+ def http(options = {})
60
+ @http ||= begin
61
+ config = self.class.default_http_options
62
+ config.each do |key, sub_options|
63
+ next if key == :adapter
64
+ sub_hash = options[key] ||= {}
65
+ sub_options.each do |sub_key, sub_value|
66
+ sub_hash[sub_key] ||= sub_value
67
+ end
68
+ end
69
+ options[:ssl][:ca_file] ||= ca_file
70
+
71
+ Faraday.new(options) do |b|
72
+ b.use(CC::Service::ResponseCheck)
73
+ b.request(:url_encoded)
74
+ b.adapter(*Array(options[:adapter] || config[:adapter]))
75
+ end
76
+ end
77
+ end
78
+
79
+ # Gets the path to the SSL Certificate Authority certs. These were taken
80
+ # from: http://curl.haxx.se/ca/cacert.pem
81
+ #
82
+ # Returns a String path.
83
+ def ca_file
84
+ @ca_file ||= ENV.fetch("CODECLIMATE_CA_FILE", File.expand_path('../../../../config/cacert.pem', __FILE__))
85
+ end
86
+
87
+ end
@@ -0,0 +1,15 @@
1
+ class CC::Service::Invocation
2
+ class InvocationChain
3
+ def initialize(&block)
4
+ @invocation = block
5
+ end
6
+
7
+ def wrap(klass, *args)
8
+ @invocation = klass.new(@invocation, *args)
9
+ end
10
+
11
+ def call
12
+ @invocation.call
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,45 @@
1
+ class CC::Service::Invocation
2
+ class WithErrorHandling
3
+ def initialize(invocation, logger, prefix = nil)
4
+ @invocation = invocation
5
+ @logger = logger
6
+ @prefix = prefix
7
+ end
8
+
9
+ def call
10
+ @invocation.call
11
+ rescue CC::Service::HTTPError => e
12
+ @logger.error(error_message(e))
13
+ {
14
+ ok: false,
15
+ params: e.params,
16
+ status: e.status,
17
+ endpoint_url: e.endpoint_url,
18
+ message: e.user_message || e.message,
19
+ log_message: error_message(e)
20
+ }
21
+ rescue => e
22
+ @logger.error(error_message(e))
23
+ {
24
+ ok: false,
25
+ message: e.message,
26
+ log_message: error_message(e)
27
+ }
28
+ end
29
+
30
+ private
31
+
32
+ def error_message(e)
33
+ if e.respond_to?(:response_body)
34
+ response_body = ". Response: <#{e.response_body.inspect}>"
35
+ else
36
+ response_body = ""
37
+ end
38
+
39
+ message = "Exception invoking service:"
40
+ message << " [#{@prefix}]" if @prefix
41
+ message << " (#{e.class}) #{e.message}"
42
+ message << response_body
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,37 @@
1
+ class CC::Service::Invocation
2
+ class WithMetrics
3
+ def initialize(invocation, statsd, prefix = nil)
4
+ @invocation = invocation
5
+ @statsd = statsd
6
+ @prefix = prefix
7
+ end
8
+
9
+ def call
10
+ start_time = Time.now
11
+
12
+ result = @invocation.call
13
+ @statsd.increment(success_key)
14
+
15
+ result
16
+ rescue => ex
17
+ @statsd.increment(error_key(ex))
18
+ raise ex
19
+ ensure
20
+ duration = ((Time.now - start_time) * 1_000).round
21
+ @statsd.timing(timing_key, duration)
22
+ end
23
+
24
+ def success_key
25
+ ["services.invocations", @prefix].compact.join('.')
26
+ end
27
+
28
+ def timing_key
29
+ ["services.timing", @prefix].compact.join('.')
30
+ end
31
+
32
+ def error_key(ex)
33
+ error_string = ex.class.name.underscore.gsub("/", "-")
34
+ ["services.errors", @prefix, error_string].compact.join('.')
35
+ end
36
+ end
37
+ end