codeclimate-services 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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