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
@@ -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" />&nbsp;<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
@@ -0,0 +1,5 @@
1
+ module CC
2
+ module Services
3
+ VERSION = "0.3.0"
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ require "cc/services/version"
2
+
3
+ require "faraday"
4
+ require "nokogiri"
5
+ require "virtus"
6
+ require "active_model"
7
+ require "active_support/core_ext"
8
+
9
+ require File.expand_path('../service', __FILE__)
@@ -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