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

Commit 640acba

Browse files
committed
Merge pull request #40 from codeclimate/slack_snapshot_notification
Use `receive_snapshot` to send notifications to Slack.
2 parents ee3ae5b + 6ce43cd commit 640acba

File tree

7 files changed

+327
-33
lines changed

7 files changed

+327
-33
lines changed

Rakefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ require 'rake/testtask'
33

44
Rake::TestTask.new do |t|
55
t.libs.push "lib"
6-
t.test_files = FileList['test/*_test.rb']
6+
t.libs.push "test"
7+
t.test_files = FileList['test/**/*_test.rb']
78
t.verbose = true
89
end
910

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

lib/cc/helpers/quality_helper.rb

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,22 @@ def previous_remediation_cost
2323
payload.fetch("previous_remediation_cost", 0)
2424
end
2525

26-
def with_article(letter)
26+
def with_article(letter, bold = false)
2727
letter ||= '?'
2828

29-
if %w( A F ).include?(letter)
30-
"an #{letter}"
29+
text = bold ? "*#{letter}*" : letter
30+
if %w( A F ).include?(letter.to_s)
31+
"an #{text}"
3132
else
32-
"a #{letter}"
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
3342
end
3443
end
3544
end

lib/cc/service/helper.rb

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
module CC::Service::Helper
2+
GREEN_HEX = "#38ae6f"
3+
RED_HEX = "#ed2f00"
24

35
def repo_name
46
payload["repo_name"]
@@ -30,9 +32,9 @@ def color
3032

3133
def hex_color
3234
if improved?
33-
"#38ae6f"
35+
GREEN_HEX
3436
else
35-
"#ed2f00"
37+
RED_HEX
3638
end
3739
end
3840

lib/cc/services/slack.rb

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1+
# encoding: UTF-8
2+
13
class CC::Service::Slack < CC::Service
4+
include CC::Service::QualityHelper
5+
26
class Config < CC::Service::Config
37
attribute :webhook_url, String,
48
label: "Webhook URL",
@@ -13,17 +17,20 @@ class Config < CC::Service::Config
1317
def receive_test
1418
speak(formatter.format_test)
1519

20+
# payloads for test receivers include the weekly quality report.
21+
send_snapshot_to_slack(CC::Formatters::SnapshotFormatter::Sample.new(payload))
22+
1623
{ ok: true, message: "Test message sent" }
1724
rescue => ex
1825
{ ok: false, message: ex.message }
1926
end
2027

21-
def receive_coverage
22-
speak(formatter.format_coverage, hex_color)
28+
def receive_snapshot
29+
send_snapshot_to_slack(CC::Formatters::SnapshotFormatter::Base.new(payload))
2330
end
2431

25-
def receive_quality
26-
speak(formatter.format_quality, hex_color)
32+
def receive_coverage
33+
speak(formatter.format_coverage, hex_color)
2734
end
2835

2936
def receive_vulnerability
@@ -51,4 +58,61 @@ def speak(message, color = nil)
5158
http.headers['Content-Type'] = 'application/json'
5259
http_post(config.webhook_url, body.to_json)
5360
end
61+
62+
def send_snapshot_to_slack(snapshot)
63+
if snapshot.alert_constants_payload
64+
speak(alerts_message(snapshot.alert_constants_payload), RED_HEX)
65+
end
66+
67+
if snapshot.improved_constants_payload
68+
speak(improvements_message(snapshot.improved_constants_payload), GREEN_HEX)
69+
end
70+
end
71+
72+
def alerts_message(constants_payload)
73+
constants = constants_payload["constants"]
74+
message = ["Quality alert triggered for *#{repo_name}* (<#{compare_url}|Compare>)\n"]
75+
76+
constants[0..2].each do |constant|
77+
object_identifier = constant_basename(constant["name"])
78+
79+
if constant["from"]
80+
from_rating = constant["from"]["rating"]
81+
to_rating = constant["to"]["rating"]
82+
83+
message << "• _#{object_identifier}_ just declined from #{with_article(from_rating, :bold)} to #{with_article(to_rating, :bold)}"
84+
else
85+
rating = constant["to"]["rating"]
86+
87+
message << "• _#{object_identifier}_ was just created and is #{with_article(rating, :bold)}"
88+
end
89+
end
90+
91+
if constants.size > 3
92+
remaining = constants.size - 3
93+
message << "\nAnd <#{details_url}|#{remaining} other #{"change".pluralize(remaining)}>"
94+
end
95+
96+
message.join("\n")
97+
end
98+
99+
def improvements_message(constants_payload)
100+
constants = constants_payload["constants"]
101+
message = ["Quality improvements in *#{repo_name}* (<#{compare_url}|Compare>)\n"]
102+
103+
constants[0..2].each do |constant|
104+
object_identifier = constant_basename(constant["name"])
105+
from_rating = constant["from"]["rating"]
106+
to_rating = constant["to"]["rating"]
107+
108+
message << "• _#{object_identifier}_ just improved from #{with_article(from_rating, :bold)} to #{with_article(to_rating, :bold)}"
109+
end
110+
111+
if constants.size > 3
112+
remaining = constants.size - 3
113+
message << "\nAnd <#{details_url}|#{remaining} other #{"improvement".pluralize(remaining)}>"
114+
end
115+
116+
message.join("\n")
117+
end
54118
end
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
require "helper"
2+
3+
class TestSnapshotFormatter < Test::Unit::TestCase
4+
def described_class
5+
CC::Formatters::SnapshotFormatter::Base
6+
end
7+
8+
def test_quality_alert_with_new_constants
9+
f = described_class.new({"new_constants" => [{"to" => {"rating" => "D"}}], "changed_constants" => []})
10+
refute_nil f.alert_constants_payload
11+
end
12+
13+
def test_quality_alert_with_decreased_constants
14+
f = described_class.new({"new_constants" => [],
15+
"changed_constants" => [{"to" => {"rating" => "D"}, "from" => {"rating" => "A"}}]
16+
})
17+
refute_nil f.alert_constants_payload
18+
end
19+
20+
def test_quality_improvements_with_better_ratings
21+
f = described_class.new({"new_constants" => [],
22+
"changed_constants" => [{"to" => {"rating" => "A"}, "from" => {"rating" => "D"}}]
23+
})
24+
refute_nil f.improved_constants_payload
25+
end
26+
27+
def test_nothing_set_without_changes
28+
f = described_class.new({"new_constants" => [], "changed_constants" => []})
29+
assert_nil f.alert_constants_payload
30+
assert_nil f.improved_constants_payload
31+
end
32+
33+
def test_snapshot_formatter_test_with_relaxed_constraints
34+
f = CC::Formatters::SnapshotFormatter::Sample.new({
35+
"new_constants" => [{"name" => "foo", "to" => {"rating" => "A"}}, {"name" => "bar", "to" => {"rating" => "A"}}],
36+
"changed_constants" => [
37+
{"from" => {"rating" => "B"}, "to" => {"rating" => "C"}},
38+
{"from" => {"rating" => "D"}, "to" => {"rating" => "D"}},
39+
{"from" => {"rating" => "D"}, "to" => {"rating" => "D"}},
40+
{"from" => {"rating" => "A"}, "to" => {"rating" => "B"}},
41+
{"from" => {"rating" => "C"}, "to" => {"rating" => "B"}}
42+
]})
43+
44+
refute_nil f.alert_constants_payload
45+
refute_nil f.improved_constants_payload
46+
end
47+
end

0 commit comments

Comments
 (0)