codeclimate-services 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.ruby-version +1 -0
- data/.travis.yml +13 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +121 -0
- data/Rakefile +11 -0
- data/bin/bundler +16 -0
- data/bin/coderay +16 -0
- data/bin/nokogiri +16 -0
- data/bin/pry +16 -0
- data/bin/rake +16 -0
- data/codeclimate-services.gemspec +29 -0
- data/config/cacert.pem +4026 -0
- data/config/load.rb +4 -0
- data/lib/axiom/types/password.rb +7 -0
- data/lib/cc/formatters/linked_formatter.rb +60 -0
- data/lib/cc/formatters/plain_formatter.rb +36 -0
- data/lib/cc/formatters/snapshot_formatter.rb +101 -0
- data/lib/cc/formatters/ticket_formatter.rb +27 -0
- data/lib/cc/helpers/coverage_helper.rb +25 -0
- data/lib/cc/helpers/issue_helper.rb +9 -0
- data/lib/cc/helpers/quality_helper.rb +44 -0
- data/lib/cc/helpers/vulnerability_helper.rb +31 -0
- data/lib/cc/presenters/github_pull_requests_presenter.rb +54 -0
- data/lib/cc/service/config.rb +4 -0
- data/lib/cc/service/formatter.rb +34 -0
- data/lib/cc/service/helper.rb +54 -0
- data/lib/cc/service/http.rb +87 -0
- data/lib/cc/service/invocation/invocation_chain.rb +15 -0
- data/lib/cc/service/invocation/with_error_handling.rb +45 -0
- data/lib/cc/service/invocation/with_metrics.rb +37 -0
- data/lib/cc/service/invocation/with_retries.rb +17 -0
- data/lib/cc/service/invocation/with_return_values.rb +18 -0
- data/lib/cc/service/invocation.rb +57 -0
- data/lib/cc/service/response_check.rb +42 -0
- data/lib/cc/service.rb +127 -0
- data/lib/cc/services/asana.rb +90 -0
- data/lib/cc/services/campfire.rb +55 -0
- data/lib/cc/services/flowdock.rb +61 -0
- data/lib/cc/services/github_issues.rb +80 -0
- data/lib/cc/services/github_pull_requests.rb +210 -0
- data/lib/cc/services/hipchat.rb +57 -0
- data/lib/cc/services/jira.rb +93 -0
- data/lib/cc/services/lighthouse.rb +79 -0
- data/lib/cc/services/pivotal_tracker.rb +78 -0
- data/lib/cc/services/slack.rb +124 -0
- data/lib/cc/services/version.rb +5 -0
- data/lib/cc/services.rb +9 -0
- data/pull_request_test.rb +47 -0
- data/service_test.rb +86 -0
- data/test/asana_test.rb +85 -0
- data/test/axiom/types/password_test.rb +22 -0
- data/test/campfire_test.rb +144 -0
- data/test/fixtures.rb +68 -0
- data/test/flowdock_test.rb +148 -0
- data/test/formatters/snapshot_formatter_test.rb +47 -0
- data/test/github_issues_test.rb +96 -0
- data/test/github_pull_requests_test.rb +293 -0
- data/test/helper.rb +50 -0
- data/test/hipchat_test.rb +130 -0
- data/test/invocation_error_handling_test.rb +51 -0
- data/test/invocation_return_values_test.rb +21 -0
- data/test/invocation_test.rb +167 -0
- data/test/jira_test.rb +80 -0
- data/test/lighthouse_test.rb +74 -0
- data/test/pivotal_tracker_test.rb +73 -0
- data/test/presenters/github_pull_requests_presenter_test.rb +49 -0
- data/test/service_test.rb +63 -0
- data/test/slack_test.rb +222 -0
- data/test/support/fake_logger.rb +11 -0
- data/test/with_metrics_test.rb +19 -0
- metadata +263 -0
@@ -0,0 +1,210 @@
|
|
1
|
+
require "cc/presenters/github_pull_requests_presenter"
|
2
|
+
|
3
|
+
class CC::Service::GitHubPullRequests < CC::Service
|
4
|
+
class Config < CC::Service::Config
|
5
|
+
attribute :oauth_token, String,
|
6
|
+
label: "OAuth Token",
|
7
|
+
description: "A personal OAuth token with permissions for the repo. The owner of the token will be the author of the pull request comment."
|
8
|
+
attribute :update_status, Boolean,
|
9
|
+
label: "Update status?",
|
10
|
+
description: "Update the pull request status after analyzing?"
|
11
|
+
attribute :add_comment, Boolean,
|
12
|
+
label: "Add a comment?",
|
13
|
+
description: "Comment on the pull request after analyzing?"
|
14
|
+
|
15
|
+
validates :oauth_token, presence: true
|
16
|
+
end
|
17
|
+
|
18
|
+
self.title = "GitHub Pull Requests"
|
19
|
+
self.description = "Update pull requests on GitHub"
|
20
|
+
|
21
|
+
BASE_URL = "https://api.github.com"
|
22
|
+
BODY_REGEX = %r{<b>Code Climate</b> has <a href=".*">analyzed this pull request</a>}
|
23
|
+
COMMENT_BODY = '<img src="https://codeclimate.com/favicon.png" width="20" height="20" /> <b>Code Climate</b> has <a href="%s">analyzed this pull request</a>.'
|
24
|
+
MESSAGES = [
|
25
|
+
DEFAULT_ERROR = "Code Climate encountered an error attempting to analyze this pull request",
|
26
|
+
]
|
27
|
+
|
28
|
+
# Just make sure we can access GH using the configured token. Without
|
29
|
+
# additional information (github-slug, PR number, etc) we can't test much
|
30
|
+
# else.
|
31
|
+
def receive_test
|
32
|
+
setup_http
|
33
|
+
|
34
|
+
if config.update_status && config.add_comment
|
35
|
+
receive_test_status
|
36
|
+
receive_test_comment
|
37
|
+
elsif config.update_status
|
38
|
+
receive_test_status
|
39
|
+
elsif config.add_comment
|
40
|
+
receive_test_comment
|
41
|
+
else
|
42
|
+
simple_failure("Nothing happened")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def receive_pull_request
|
47
|
+
setup_http
|
48
|
+
state = @payload["state"]
|
49
|
+
|
50
|
+
if %w[pending success failure skipped error].include?(state)
|
51
|
+
send("update_status_#{state}")
|
52
|
+
else
|
53
|
+
@response = simple_failure("Unknown state")
|
54
|
+
end
|
55
|
+
|
56
|
+
response
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def simple_failure(message)
|
62
|
+
{ ok: false, message: message }
|
63
|
+
end
|
64
|
+
|
65
|
+
def response
|
66
|
+
@response || simple_failure("Nothing happened")
|
67
|
+
end
|
68
|
+
|
69
|
+
def update_status_skipped
|
70
|
+
update_status(
|
71
|
+
"success",
|
72
|
+
"Code Climate has skipped analysis of this commit."
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
76
|
+
def update_status_success
|
77
|
+
add_comment
|
78
|
+
update_status("success", presenter.success_message)
|
79
|
+
end
|
80
|
+
|
81
|
+
def update_status_failure
|
82
|
+
add_comment
|
83
|
+
update_status("failure", presenter.success_message)
|
84
|
+
end
|
85
|
+
|
86
|
+
def presenter
|
87
|
+
CC::Service::GitHubPullRequestsPresenter.new(@payload)
|
88
|
+
end
|
89
|
+
|
90
|
+
def update_status_error
|
91
|
+
update_status(
|
92
|
+
"error",
|
93
|
+
@payload["message"] || DEFAULT_ERROR
|
94
|
+
)
|
95
|
+
end
|
96
|
+
|
97
|
+
def update_status_pending
|
98
|
+
update_status("pending", "Code Climate is analyzing this code.")
|
99
|
+
end
|
100
|
+
|
101
|
+
def update_status(state, description)
|
102
|
+
if config.update_status
|
103
|
+
params = {
|
104
|
+
state: state,
|
105
|
+
description: description,
|
106
|
+
target_url: @payload["details_url"],
|
107
|
+
context: "codeclimate"
|
108
|
+
}
|
109
|
+
if state == "error"
|
110
|
+
params.delete(:target_url)
|
111
|
+
end
|
112
|
+
@response = service_post(status_url, params.to_json)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def add_comment
|
117
|
+
if config.add_comment
|
118
|
+
if !comment_present?
|
119
|
+
body = {
|
120
|
+
body: COMMENT_BODY % @payload["compare_url"]
|
121
|
+
}.to_json
|
122
|
+
|
123
|
+
@response = service_post(comments_url, body) do |response|
|
124
|
+
doc = JSON.parse(response.body)
|
125
|
+
{ id: doc["id"] }
|
126
|
+
end
|
127
|
+
else
|
128
|
+
@response = {
|
129
|
+
ok: true,
|
130
|
+
message: "Comment already present"
|
131
|
+
}
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def receive_test_status
|
137
|
+
url = base_status_url("0" * 40)
|
138
|
+
params = {}
|
139
|
+
raw_post(url, params.to_json)
|
140
|
+
rescue CC::Service::HTTPError => e
|
141
|
+
if e.status == 422
|
142
|
+
{
|
143
|
+
ok: true,
|
144
|
+
params: params.as_json,
|
145
|
+
status: e.status,
|
146
|
+
endpoint_url: url,
|
147
|
+
message: "OAuth token is valid"
|
148
|
+
}
|
149
|
+
else
|
150
|
+
raise
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def receive_test_comment
|
155
|
+
response = service_get(user_url)
|
156
|
+
if response_includes_repo_scope?(response)
|
157
|
+
{ ok: true, message: "OAuth token is valid" }
|
158
|
+
else
|
159
|
+
{ ok: false, message: "OAuth token requires 'repo' scope to post comments." }
|
160
|
+
end
|
161
|
+
rescue => ex
|
162
|
+
{ ok: false, message: ex.message }
|
163
|
+
end
|
164
|
+
|
165
|
+
def comment_present?
|
166
|
+
response = service_get(comments_url)
|
167
|
+
comments = JSON.parse(response.body)
|
168
|
+
|
169
|
+
comments.any? { |comment| comment["body"] =~ BODY_REGEX }
|
170
|
+
end
|
171
|
+
|
172
|
+
def setup_http
|
173
|
+
http.headers["Content-Type"] = "application/json"
|
174
|
+
http.headers["Authorization"] = "token #{config.oauth_token}"
|
175
|
+
http.headers["User-Agent"] = "Code Climate"
|
176
|
+
end
|
177
|
+
|
178
|
+
def status_url
|
179
|
+
base_status_url(commit_sha)
|
180
|
+
end
|
181
|
+
|
182
|
+
def base_status_url(commit_sha)
|
183
|
+
"#{BASE_URL}/repos/#{github_slug}/statuses/#{commit_sha}"
|
184
|
+
end
|
185
|
+
|
186
|
+
def comments_url
|
187
|
+
"#{BASE_URL}/repos/#{github_slug}/issues/#{number}/comments"
|
188
|
+
end
|
189
|
+
|
190
|
+
def user_url
|
191
|
+
"#{BASE_URL}/user"
|
192
|
+
end
|
193
|
+
|
194
|
+
def github_slug
|
195
|
+
@payload.fetch("github_slug")
|
196
|
+
end
|
197
|
+
|
198
|
+
def commit_sha
|
199
|
+
@payload.fetch("commit_sha")
|
200
|
+
end
|
201
|
+
|
202
|
+
def number
|
203
|
+
@payload.fetch("number")
|
204
|
+
end
|
205
|
+
|
206
|
+
def response_includes_repo_scope?(response)
|
207
|
+
response.headers['x-oauth-scopes'] && response.headers['x-oauth-scopes'].split(/\s*,\s*/).include?("repo")
|
208
|
+
end
|
209
|
+
|
210
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
class CC::Service::HipChat < CC::Service
|
2
|
+
class Config < CC::Service::Config
|
3
|
+
attribute :auth_token, String,
|
4
|
+
description: "Your HipChat API auth token"
|
5
|
+
|
6
|
+
attribute :room_id, String,
|
7
|
+
description: "The ID or name of the HipChat chat room to send notifications to"
|
8
|
+
|
9
|
+
attribute :notify, Boolean, default: false,
|
10
|
+
description: "Should we trigger a notification for people in the room?"
|
11
|
+
|
12
|
+
validates :auth_token, presence: true
|
13
|
+
validates :room_id, presence: true
|
14
|
+
end
|
15
|
+
|
16
|
+
BASE_URL = "https://api.hipchat.com/v1"
|
17
|
+
|
18
|
+
self.description = "Send messages to a HipChat chat room"
|
19
|
+
|
20
|
+
def receive_test
|
21
|
+
speak(formatter.format_test, "green").merge(
|
22
|
+
message: "Test message sent"
|
23
|
+
)
|
24
|
+
end
|
25
|
+
|
26
|
+
def receive_coverage
|
27
|
+
speak(formatter.format_coverage, color)
|
28
|
+
end
|
29
|
+
|
30
|
+
def receive_quality
|
31
|
+
speak(formatter.format_quality, color)
|
32
|
+
end
|
33
|
+
|
34
|
+
def receive_vulnerability
|
35
|
+
speak(formatter.format_vulnerability, "red")
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def formatter
|
41
|
+
CC::Formatters::LinkedFormatter.new(self, prefix: nil, link_style: :html)
|
42
|
+
end
|
43
|
+
|
44
|
+
def speak(message, color)
|
45
|
+
url = "#{BASE_URL}/rooms/message"
|
46
|
+
params = {
|
47
|
+
from: "Code Climate",
|
48
|
+
message: message,
|
49
|
+
auth_token: config.auth_token,
|
50
|
+
room_id: config.room_id,
|
51
|
+
notify: !!config.notify,
|
52
|
+
color: color
|
53
|
+
}
|
54
|
+
service_post(url, params)
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'base64'
|
2
|
+
|
3
|
+
class CC::Service::Jira < CC::Service
|
4
|
+
class Config < CC::Service::Config
|
5
|
+
attribute :domain, String,
|
6
|
+
description: "Your JIRA host domain (e.g. yourjira.com:PORT, please exclude https://)"
|
7
|
+
|
8
|
+
attribute :username, String,
|
9
|
+
description: "Must exactly match the 'username' that appears on your JIRA profile page."
|
10
|
+
|
11
|
+
attribute :password, Password,
|
12
|
+
label: "JIRA password",
|
13
|
+
description: "Your JIRA password"
|
14
|
+
|
15
|
+
attribute :project_id, String,
|
16
|
+
description: "Your JIRA project ID number (located in your JIRA admin panel). Project must support 'task' issue types and contain only the default required fields."
|
17
|
+
|
18
|
+
attribute :labels, String,
|
19
|
+
description: "Which labels to add to issues, comma delimited"
|
20
|
+
|
21
|
+
validates :domain, presence: true
|
22
|
+
validates :username, presence: true
|
23
|
+
validates :password, presence: true
|
24
|
+
validates :project_id, presence: true
|
25
|
+
end
|
26
|
+
|
27
|
+
self.title = "JIRA"
|
28
|
+
self.description = "Create tickets in JIRA"
|
29
|
+
self.issue_tracker = true
|
30
|
+
|
31
|
+
def receive_test
|
32
|
+
result = create_ticket("Test ticket from Code Climate", "")
|
33
|
+
result.merge(
|
34
|
+
message: "Ticket <a href='#{result[:url]}'>#{result[:id]}</a> created."
|
35
|
+
)
|
36
|
+
end
|
37
|
+
|
38
|
+
def receive_quality
|
39
|
+
title = "Refactor #{constant_name} from #{rating} on Code Climate"
|
40
|
+
|
41
|
+
create_ticket(title, details_url)
|
42
|
+
end
|
43
|
+
|
44
|
+
def receive_issue
|
45
|
+
title = %{Fix "#{issue["check_name"]}" issue in #{constant_name}}
|
46
|
+
|
47
|
+
body = [issue["description"], details_url].join("\n\n")
|
48
|
+
|
49
|
+
create_ticket(title, body)
|
50
|
+
end
|
51
|
+
|
52
|
+
def receive_vulnerability
|
53
|
+
formatter = CC::Formatters::TicketFormatter.new(self)
|
54
|
+
|
55
|
+
create_ticket(
|
56
|
+
formatter.format_vulnerability_title,
|
57
|
+
formatter.format_vulnerability_body
|
58
|
+
)
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def create_ticket(title, ticket_body)
|
64
|
+
params = {
|
65
|
+
fields:
|
66
|
+
{
|
67
|
+
project: { id: config.project_id },
|
68
|
+
summary: title,
|
69
|
+
description: ticket_body,
|
70
|
+
issuetype: { name: "Task" }
|
71
|
+
}
|
72
|
+
}
|
73
|
+
|
74
|
+
if config.labels.present?
|
75
|
+
params[:fields][:labels] = config.labels.split(",")
|
76
|
+
end
|
77
|
+
|
78
|
+
http.headers["Content-Type"] = "application/json"
|
79
|
+
http.basic_auth(config.username, config.password)
|
80
|
+
|
81
|
+
url = "https://#{config.domain}/rest/api/2/issue/"
|
82
|
+
|
83
|
+
service_post(url, params.to_json) do |response|
|
84
|
+
body = JSON.parse(response.body)
|
85
|
+
{
|
86
|
+
id: body["id"],
|
87
|
+
key: body["key"],
|
88
|
+
url: body["self"]
|
89
|
+
}
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
class CC::Service::Lighthouse < CC::Service
|
2
|
+
class Config < CC::Service::Config
|
3
|
+
attribute :subdomain, String,
|
4
|
+
description: "Your Lighthouse subdomain"
|
5
|
+
|
6
|
+
attribute :api_token, String,
|
7
|
+
label: "API Token",
|
8
|
+
description: "Your Lighthouse API Key (http://help.lighthouseapp.com/kb/api/how-do-i-get-an-api-token)"
|
9
|
+
|
10
|
+
attribute :project_id, String,
|
11
|
+
description: "Your Lighthouse project ID. You can find this from the URL to your Lighthouse project."
|
12
|
+
|
13
|
+
attribute :tags, String,
|
14
|
+
description: "Which tags to add to tickets, comma delimited"
|
15
|
+
|
16
|
+
validates :subdomain, presence: true
|
17
|
+
validates :api_token, presence: true
|
18
|
+
validates :project_id, presence: true
|
19
|
+
end
|
20
|
+
|
21
|
+
self.title = "Lighthouse"
|
22
|
+
self.description = "Create tickets in Lighthouse"
|
23
|
+
self.issue_tracker = true
|
24
|
+
|
25
|
+
def receive_test
|
26
|
+
result = create_ticket("Test ticket from Code Climate", "")
|
27
|
+
result.merge(
|
28
|
+
message: "Ticket <a href='#{result[:url]}'>#{result[:id]}</a> created."
|
29
|
+
)
|
30
|
+
end
|
31
|
+
|
32
|
+
def receive_quality
|
33
|
+
title = "Refactor #{constant_name} from #{rating} on Code Climate"
|
34
|
+
|
35
|
+
create_ticket(title, details_url)
|
36
|
+
end
|
37
|
+
|
38
|
+
def receive_issue
|
39
|
+
title = %{Fix "#{issue["check_name"]}" issue in #{constant_name}}
|
40
|
+
|
41
|
+
body = [issue["description"], details_url].join("\n\n")
|
42
|
+
|
43
|
+
create_ticket(title, body)
|
44
|
+
end
|
45
|
+
|
46
|
+
def receive_vulnerability
|
47
|
+
formatter = CC::Formatters::TicketFormatter.new(self)
|
48
|
+
|
49
|
+
create_ticket(
|
50
|
+
formatter.format_vulnerability_title,
|
51
|
+
formatter.format_vulnerability_body
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def create_ticket(title, ticket_body)
|
58
|
+
params = { ticket: { title: title, body: ticket_body } }
|
59
|
+
|
60
|
+
if config.tags.present?
|
61
|
+
params[:ticket][:tags] = config.tags.strip
|
62
|
+
end
|
63
|
+
|
64
|
+
http.headers["X-LighthouseToken"] = config.api_token
|
65
|
+
http.headers["Content-Type"] = "application/json"
|
66
|
+
|
67
|
+
base_url = "https://#{config.subdomain}.lighthouseapp.com"
|
68
|
+
url = "#{base_url}/projects/#{config.project_id}/tickets.json"
|
69
|
+
|
70
|
+
service_post(url, params.to_json) do |response|
|
71
|
+
body = JSON.parse(response.body)
|
72
|
+
{
|
73
|
+
id: body["ticket"]["number"],
|
74
|
+
url: body["ticket"]["url"],
|
75
|
+
}
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
class CC::Service::PivotalTracker < CC::Service
|
2
|
+
class Config < CC::Service::Config
|
3
|
+
attribute :api_token, String,
|
4
|
+
description: "Your Pivotal Tracker API Token, from your profile page"
|
5
|
+
|
6
|
+
attribute :project_id, String,
|
7
|
+
description: "Your Pivotal Tracker project ID"
|
8
|
+
|
9
|
+
attribute :labels, String,
|
10
|
+
label: "Labels (comma separated)",
|
11
|
+
description: "Comma separated list of labels to apply to the story"
|
12
|
+
|
13
|
+
validates :api_token, presence: true
|
14
|
+
validates :project_id, presence: true
|
15
|
+
end
|
16
|
+
|
17
|
+
self.title = "Pivotal Tracker"
|
18
|
+
self.description = "Create stories on Pivotal Tracker"
|
19
|
+
self.issue_tracker = true
|
20
|
+
|
21
|
+
BASE_URL = "https://www.pivotaltracker.com/services/v3"
|
22
|
+
|
23
|
+
def receive_test
|
24
|
+
result = create_story("Test ticket from Code Climate", "")
|
25
|
+
result.merge(
|
26
|
+
message: "Ticket <a href='#{result[:url]}'>#{result[:id]}</a> created."
|
27
|
+
)
|
28
|
+
end
|
29
|
+
|
30
|
+
def receive_quality
|
31
|
+
name = "Refactor #{constant_name} from #{rating} on Code Climate"
|
32
|
+
|
33
|
+
create_story(name, details_url)
|
34
|
+
end
|
35
|
+
|
36
|
+
def receive_issue
|
37
|
+
title = %{Fix "#{issue["check_name"]}" issue in #{constant_name}}
|
38
|
+
|
39
|
+
body = [issue["description"], details_url].join("\n\n")
|
40
|
+
|
41
|
+
create_story(title, body)
|
42
|
+
end
|
43
|
+
|
44
|
+
def receive_vulnerability
|
45
|
+
formatter = CC::Formatters::TicketFormatter.new(self)
|
46
|
+
|
47
|
+
create_story(
|
48
|
+
formatter.format_vulnerability_title,
|
49
|
+
formatter.format_vulnerability_body
|
50
|
+
)
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def create_story(name, description)
|
56
|
+
params = {
|
57
|
+
"story[name]" => name,
|
58
|
+
"story[story_type]" => "chore",
|
59
|
+
"story[description]" => description,
|
60
|
+
}
|
61
|
+
|
62
|
+
if config.labels.present?
|
63
|
+
params["story[labels]"] = config.labels.strip
|
64
|
+
end
|
65
|
+
|
66
|
+
http.headers["X-TrackerToken"] = config.api_token
|
67
|
+
url = "#{BASE_URL}/projects/#{config.project_id}/stories"
|
68
|
+
|
69
|
+
service_post(url, params) do |response|
|
70
|
+
body = Nokogiri::XML(response.body)
|
71
|
+
{
|
72
|
+
id: (body / "story/id").text,
|
73
|
+
url: (body / "story/url").text
|
74
|
+
}
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
3
|
+
class CC::Service::Slack < CC::Service
|
4
|
+
include CC::Service::QualityHelper
|
5
|
+
|
6
|
+
class Config < CC::Service::Config
|
7
|
+
attribute :webhook_url, String,
|
8
|
+
label: "Webhook URL",
|
9
|
+
description: "The Slack webhook URL you would like message posted to"
|
10
|
+
|
11
|
+
attribute :channel, String,
|
12
|
+
description: "The channel to send to (optional). Enter # before the channel name."
|
13
|
+
end
|
14
|
+
|
15
|
+
self.description = "Send messages to a Slack channel"
|
16
|
+
|
17
|
+
def receive_test
|
18
|
+
# payloads for test receivers include the weekly quality report.
|
19
|
+
send_snapshot_to_slack(CC::Formatters::SnapshotFormatter::Sample.new(payload))
|
20
|
+
speak(formatter.format_test).merge(
|
21
|
+
message: "Test message sent"
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
def receive_snapshot
|
26
|
+
send_snapshot_to_slack(CC::Formatters::SnapshotFormatter::Base.new(payload))
|
27
|
+
end
|
28
|
+
|
29
|
+
def receive_coverage
|
30
|
+
speak(formatter.format_coverage, hex_color)
|
31
|
+
end
|
32
|
+
|
33
|
+
def receive_vulnerability
|
34
|
+
speak(formatter.format_vulnerability)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def formatter
|
40
|
+
CC::Formatters::LinkedFormatter.new(self, prefix: nil, link_style: :wiki)
|
41
|
+
end
|
42
|
+
|
43
|
+
def speak(message, color = nil)
|
44
|
+
params = { attachments: [{
|
45
|
+
color: color,
|
46
|
+
fallback: message,
|
47
|
+
fields: [{ value: message }],
|
48
|
+
mrkdwn_in: ["fields", "fallback"]
|
49
|
+
}]}
|
50
|
+
|
51
|
+
if config.channel
|
52
|
+
params[:channel] = config.channel
|
53
|
+
end
|
54
|
+
|
55
|
+
http.headers['Content-Type'] = 'application/json'
|
56
|
+
url = config.webhook_url
|
57
|
+
|
58
|
+
service_post(url, params.to_json) do |response|
|
59
|
+
{
|
60
|
+
ok: response.body == "ok",
|
61
|
+
message: response.body
|
62
|
+
}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def send_snapshot_to_slack(snapshot)
|
67
|
+
if snapshot.alert_constants_payload
|
68
|
+
@response = speak(alerts_message(snapshot.alert_constants_payload), RED_HEX)
|
69
|
+
end
|
70
|
+
|
71
|
+
if snapshot.improved_constants_payload
|
72
|
+
@response = speak(improvements_message(snapshot.improved_constants_payload), GREEN_HEX)
|
73
|
+
end
|
74
|
+
|
75
|
+
@response || { ok: false, ignored: true, message: "No changes in snapshot" }
|
76
|
+
end
|
77
|
+
|
78
|
+
def alerts_message(constants_payload)
|
79
|
+
constants = constants_payload["constants"]
|
80
|
+
message = ["Quality alert triggered for *#{repo_name}* (<#{compare_url}|Compare>)\n"]
|
81
|
+
|
82
|
+
constants[0..2].each do |constant|
|
83
|
+
object_identifier = constant_basename(constant["name"])
|
84
|
+
|
85
|
+
if constant["from"]
|
86
|
+
from_rating = constant["from"]["rating"]
|
87
|
+
to_rating = constant["to"]["rating"]
|
88
|
+
|
89
|
+
message << "• _#{object_identifier}_ just declined from #{with_article(from_rating, :bold)} to #{with_article(to_rating, :bold)}"
|
90
|
+
else
|
91
|
+
rating = constant["to"]["rating"]
|
92
|
+
|
93
|
+
message << "• _#{object_identifier}_ was just created and is #{with_article(rating, :bold)}"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
if constants.size > 3
|
98
|
+
remaining = constants.size - 3
|
99
|
+
message << "\nAnd <#{details_url}|#{remaining} other #{"change".pluralize(remaining)}>"
|
100
|
+
end
|
101
|
+
|
102
|
+
message.join("\n")
|
103
|
+
end
|
104
|
+
|
105
|
+
def improvements_message(constants_payload)
|
106
|
+
constants = constants_payload["constants"]
|
107
|
+
message = ["Quality improvements in *#{repo_name}* (<#{compare_url}|Compare>)\n"]
|
108
|
+
|
109
|
+
constants[0..2].each do |constant|
|
110
|
+
object_identifier = constant_basename(constant["name"])
|
111
|
+
from_rating = constant["from"]["rating"]
|
112
|
+
to_rating = constant["to"]["rating"]
|
113
|
+
|
114
|
+
message << "• _#{object_identifier}_ just improved from #{with_article(from_rating, :bold)} to #{with_article(to_rating, :bold)}"
|
115
|
+
end
|
116
|
+
|
117
|
+
if constants.size > 3
|
118
|
+
remaining = constants.size - 3
|
119
|
+
message << "\nAnd <#{details_url}|#{remaining} other #{"improvement".pluralize(remaining)}>"
|
120
|
+
end
|
121
|
+
|
122
|
+
message.join("\n")
|
123
|
+
end
|
124
|
+
end
|
data/lib/cc/services.rb
ADDED
@@ -0,0 +1,47 @@
|
|
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
|
+
# OAUTH_TOKEN: Personal GitHub access token
|
10
|
+
#
|
11
|
+
# GitHub >
|
12
|
+
# Account settings >
|
13
|
+
# Applications >
|
14
|
+
# Personal access tokens >
|
15
|
+
# Generate new token
|
16
|
+
#
|
17
|
+
###
|
18
|
+
require 'cc/services'
|
19
|
+
CC::Service.load_services
|
20
|
+
|
21
|
+
class WithResponseLogging
|
22
|
+
def initialize(invocation)
|
23
|
+
@invocation = invocation
|
24
|
+
end
|
25
|
+
|
26
|
+
def call
|
27
|
+
@invocation.call.tap { |r| p r }
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
service = CC::Service::GitHubPullRequests.new({
|
32
|
+
oauth_token: ENV.fetch("OAUTH_TOKEN"),
|
33
|
+
update_status: true,
|
34
|
+
add_comment: true,
|
35
|
+
}, {
|
36
|
+
name: "pull_request",
|
37
|
+
# https://github.com/codeclimate/nillson/pull/33
|
38
|
+
state: "success",
|
39
|
+
github_slug: "codeclimate/nillson",
|
40
|
+
issue_comparison_counts: {"new" => 0, "fixed" => 0},
|
41
|
+
number: 33,
|
42
|
+
commit_sha: "986ec903b8420f4e8c8d696d8950f7bd0667ff0c"
|
43
|
+
})
|
44
|
+
|
45
|
+
CC::Service::Invocation.new(service) do |i|
|
46
|
+
i.wrap(WithResponseLogging)
|
47
|
+
end
|