codeclimate-services 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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