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.
- 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
|