prophet 1.6.1 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: d406c9bd4203b9aa5c02875eafd8295d102746d3
4
- data.tar.gz: 24e1380804b9b326c782ce7b170c95a3b9886d43
2
+ SHA256:
3
+ metadata.gz: 63a3b73db0063a2926440a4e1f19a0ae76d62be4a5ae20e7a88fb677229000b5
4
+ data.tar.gz: 7fff7b419b81c22b5f612a3523a04a73b71c53f935d1bdd7f0aca05ec4470ee5
5
5
  SHA512:
6
- metadata.gz: 5b801cca85b70e5d1509f3fa20fae331ce6e91dda9ccf35f89bb97c162f82455e56304c771edbd10ff40e9e987475495c4ec8dffc22766f4018f75e05779eff2
7
- data.tar.gz: 57e9e478771fe063f828d236705bf143d943986d28cc5228725da32f6c92408a48bc54bfb2de143fbe6101271c4fabb757626d250c2db3abb89be2245dc0b6ec
6
+ metadata.gz: 12ce2eda7bcb9e7e401b32c0f28375a7e873188d41b83dc1a6ec892dc7f4f14f64daeb79f6e3509f918f086852368e0fb2ba0b8bc7310f00d7ec4e00621409e5
7
+ data.tar.gz: 0bd79da7a4d39026bc89e3cdb31a8efcc7109cf1c98948f57e915349fa4689a162c9ac6814b54156c480aabd7561bb9e4fe22d4580eadc195b912ac642d85840
@@ -1,9 +1,12 @@
1
+ require 'English'
2
+ require 'open3'
3
+
1
4
  class Prophet
2
5
 
3
- attr_accessor :username,
4
- :password,
6
+ attr_accessor :username_pass,
7
+ :access_token_pass,
5
8
  :username_fail,
6
- :password_fail,
9
+ :access_token_fail,
7
10
  :rerun_on_source_change,
8
11
  :rerun_on_target_change,
9
12
  :prepare_block,
@@ -15,7 +18,10 @@ class Prophet
15
18
  :status_success,
16
19
  :comment_failure,
17
20
  :comment_success,
18
- :reuse_comments
21
+ :disable_comments,
22
+ :reuse_comments,
23
+ :status_context,
24
+ :status_target_url
19
25
 
20
26
  # Allow configuration blocks being passed to Prophet.
21
27
  # See the README.md for examples on how to call this method.
@@ -42,7 +48,7 @@ class Prophet
42
48
  begin
43
49
  self.prepare_block.call
44
50
  rescue Exception => e
45
- @log.error "Preparation block raised an exception: #{e}"
51
+ logger.error "Preparation block raised an exception: #{e}"
46
52
  end
47
53
  # Loop through all 'open' pull requests.
48
54
  selected_requests = pull_requests.select do |request|
@@ -53,30 +59,38 @@ class Prophet
53
59
  remove_comment unless self.reuse_comments
54
60
  true
55
61
  end
62
+
56
63
  # Run code on all selected requests.
57
64
  selected_requests.each do |request|
58
65
  @request = request
59
- @log.info "Running for request ##{@request.id}."
66
+ logger.info "Running for request ##{@request.id}."
60
67
  # GitHub always creates a merge commit for its 'Merge Button'.
61
68
  # Prophet reuses that commit to run the code on it.
62
- switch_branch_to_merged_state
63
- # Run specified code (i.e. tests) for the project.
64
- begin
65
- self.exec_block.call
66
- # Unless self.success has already been set (to true/false) manually,
67
- # the success/failure is determined by the last command's return code.
68
- self.success = ($? && $?.exitstatus == 0) if self.success.nil?
69
- rescue Exception => e
70
- @log.error "Execution block raised an exception: #{e}"
71
- self.success = false
69
+ if switch_branch_to_merged_state
70
+ # Run specified code (i.e. tests) for the project.
71
+ begin
72
+ self.exec_block.call
73
+ # Unless self.success has already been set (to true/false) manually,
74
+ # the success/failure is determined by the last command's return code.
75
+ self.success = ($CHILD_STATUS && $CHILD_STATUS.exitstatus == 0) if self.success.nil?
76
+ rescue Exception => e
77
+ logger.error "Execution block raised an exception: #{e}"
78
+ self.success = false
79
+ end
80
+ switch_branch_back
81
+ comment_on_github
82
+ set_status_on_github
72
83
  end
73
- switch_branch_back
74
- comment_on_github
75
- set_status_on_github
76
84
  self.success = nil
77
85
  end
78
86
  end
79
87
 
88
+ def logger
89
+ @logger ||= Logger.new(STDOUT).tap do |log|
90
+ log.level = Logger::INFO
91
+ end
92
+ end
93
+
80
94
 
81
95
  private
82
96
 
@@ -86,67 +100,57 @@ class Prophet
86
100
  end
87
101
 
88
102
  def configure
89
- # Use existing logger or fall back to a new one with standard log level.
90
- if self.logger
91
- @log = self.logger
92
- else
93
- @log = Logger.new(STDOUT)
94
- @log.level = Logger::INFO
95
- end
103
+ self.username_fail ||= self.username_pass
104
+ self.access_token_fail ||= self.access_token_pass
105
+
96
106
  # Set default fall back values for options that aren't set.
97
- self.username ||= git_config['github.login']
98
- self.password ||= git_config['github.password']
99
- self.username_fail ||= self.username
100
- self.password_fail ||= self.password
101
107
  self.rerun_on_source_change = true if self.rerun_on_source_change.nil?
102
108
  self.rerun_on_target_change = true if self.rerun_on_target_change.nil?
103
109
  self.reuse_comments = false if self.reuse_comments.nil?
110
+ self.disable_comments = false if self.disable_comments.nil?
104
111
  # Allow for custom messages.
105
112
  self.status_pending ||= 'Prophet is still running.'
106
113
  self.status_failure ||= 'Prophet reports failure.'
107
114
  self.status_success ||= 'Prophet reports success.'
108
115
  self.comment_failure ||= 'Prophet reports failure.'
109
116
  self.comment_success ||= 'Prophet reports success.'
117
+ self.status_context ||= 'prophet/default'
110
118
  # Find environment (tasks, project, ...).
111
119
  self.prepare_block ||= lambda {}
112
120
  self.exec_block ||= lambda { `rake` }
113
- @github = connect_to_github
114
- @github_fail = if self.username == self.username_fail
115
- @github
116
- else
117
- connect_to_github self.username_fail, self.password_fail
118
- end
121
+ @github = connect_to_github(access_token: access_token_pass)
122
+ @github_fail = connect_to_github(access_token: access_token_fail)
119
123
  end
120
124
 
121
- def connect_to_github(user = self.username, pass = self.password)
122
- github = Octokit::Client.new(
123
- :login => user,
124
- :password => pass
125
- )
125
+ def connect_to_github(access_token:)
126
+ github = Octokit::Client.new(access_token: access_token)
127
+
126
128
  # Check user login to GitHub.
127
129
  github.login
128
- @log.info "Successfully logged into GitHub (API v#{github.api_version}) with user '#{user}'."
130
+ logger.info "Successfully logged into GitHub with user '#{github.user.login}'."
131
+
129
132
  # Ensure the user has access to desired project.
130
- # NOTE: Both variants should work:
133
+ # NOTE: All three variants should work:
131
134
  # 'ssh://git@github.com:user/project.git'
132
135
  # 'git@github.com:user/project.git'
133
- @project ||= /com:(.*)\.git/.match(git_config['remote.origin.url'])[1]
136
+ # 'https://github.com/user/project.git'
137
+ @project ||= /github\.com[\/:](.*)\.git$/.match(git_config['remote.origin.url'])[1]
134
138
  begin
135
139
  github.repo @project
136
- @log.info "Successfully accessed GitHub project '#{@project}'"
140
+ logger.info "Successfully accessed GitHub project '#{@project}'"
137
141
  github
138
142
  rescue Octokit::Unauthorized => e
139
- @log.error "Unable to access GitHub project with user '#{user}':\n#{e.message}"
143
+ logger.error "Unable to access GitHub project with user '#{github.user}':\n#{e.message}"
140
144
  abort
141
145
  end
142
146
  end
143
147
 
144
148
  def pull_requests
145
- request_list = @github.pulls @project, 'open'
149
+ request_list = @github.pulls @project, state: 'open'
146
150
  requests = request_list.collect do |request|
147
151
  PullRequest.new(@github.pull_request @project, request.number)
148
152
  end
149
- @log.info "Found #{requests.size > 0 ? requests.size : 'no'} open pull requests in '#{@project}'."
153
+ logger.info "Found #{requests.size > 0 ? requests.size : 'no'} open pull requests in '#{@project}'."
150
154
  requests
151
155
  end
152
156
 
@@ -154,73 +158,91 @@ class Prophet
154
158
  # - the pull request hasn't been used for a run before.
155
159
  # - the pull request has been updated since the last run.
156
160
  # - the target (i.e. master) has been updated since the last run.
161
+ # - the pull request does not originate from a fork (to avoid malicious code execution on CI machines)
157
162
  def run_necessary?
158
- @log.info "Checking pull request ##{@request.id}: #{@request.content.title}"
159
- # Compare current sha ids of target and source branch with those from the last test run.
160
- @request.target_head_sha = @github.commits(@project).first.sha
161
- comments = @github.issue_comments(@project, @request.id)
162
- comments = comments.select { |c| [username, username_fail].include?(c.user.login) }.reverse
163
- # Initialize shas to ensure it will live on after the 'each' block.
164
- shas = nil
165
- @request.comment = nil
166
- comments.each do |comment|
167
- shas = /Merged ([\w]+) into ([\w]+)/.match(comment.body)
168
- if shas && shas[1] && shas[2]
169
- # Remember comment to be able to update or delete it later.
170
- @request.comment = comment
171
- break
163
+ logger.info "Checking pull request ##{@request.id}: #{@request.content.title}"
164
+
165
+ unless @request.from_fork || @request.wip
166
+ # Compare current sha ids of target and source branch with those from the last test run.
167
+ @request.target_head_sha = @github.commits(@project).first.sha
168
+ comments = @github.issue_comments(@project, @request.id)
169
+ comments = comments.select { |c| [username_pass, username_fail].include?(c.user.login) }.reverse
170
+ comments.each do |comment|
171
+ @request.comment = comment if /Merged ([\w]+) into ([\w]+)/.match(comment.body)
172
172
  end
173
- end
174
- # If it's not mergeable, we need to delete all comments of former test runs.
175
- unless @request.content.mergeable
176
- # Sometimes GitHub doesn't have a proper boolean value stored.
177
- if @request.content.mergeable.nil? && switch_branch_to_merged_state(false)
178
- # Pull request is mergeable after all.
179
- switch_branch_back
173
+
174
+ statuses = @github.status(@project, @request.head_sha).statuses.select { |s| s.context == self.status_context }
175
+ # Only run if it's mergeable.
176
+ if @request.content.mergeable
177
+ if statuses.empty?
178
+ # If there is no status yet, it has to be a new request.
179
+ logger.info 'New pull request detected, run needed.'
180
+ return true
181
+ elsif !self.disable_comments && !@request.comment
182
+ logger.info 'Rerun forced.'
183
+ return true
184
+ end
180
185
  else
181
- @log.info 'Pull request not auto-mergeable. Not running.'
182
- if @request.comment
183
- @log.info 'Deleting existing comment.'
184
- call_github(old_comment_success?).delete_comment(@project, @request.comment.id)
186
+ # Sometimes GitHub doesn't have a proper boolean value stored.
187
+ if @request.content.mergeable.nil? && switch_branch_to_merged_state
188
+ # Pull request is mergeable after all.
189
+ switch_branch_back
190
+ else
191
+ logger.info 'Pull request not auto-mergeable. Not running.'
192
+ if @request.comment
193
+ logger.info 'Deleting existing comment.'
194
+ call_github(old_comment_success?).delete_comment(@project, @request.comment.id)
195
+ end
196
+ create_status(:error, "Pull request not auto-mergeable. Not running.") if statuses.first && statuses.first.state != 'error'
197
+ return false
185
198
  end
186
- return false
187
199
  end
188
- end
189
- if @request.comment
190
- @log.info "Current target sha: '#{@request.target_head_sha}', pull sha: '#{@request.head_sha}'."
191
- @log.info "Last test run target sha: '#{shas[2]}', pull sha: '#{shas[1]}'."
192
- if self.rerun_on_source_change && (shas[1] != @request.head_sha)
193
- @log.info 'Re-running due to new commit in pull request.'
194
- return true
195
- elsif self.rerun_on_target_change && (shas[2] != @request.target_head_sha)
196
- @log.info 'Re-running due to new commit in target branch.'
200
+
201
+ # Initialize shas to ensure it will live on after the 'each' block.
202
+ shas = nil
203
+ statuses.each do |status|
204
+ shas = /Merged ([\w]+) into ([\w]+)/.match(status.description)
205
+ break if shas && shas[1] && shas[2]
206
+ end
207
+
208
+ if shas
209
+ logger.info "Current target sha: '#{@request.target_head_sha}', pull sha: '#{@request.head_sha}'."
210
+ logger.info "Last test run target sha: '#{shas[2]}', pull sha: '#{shas[1]}'."
211
+ if self.rerun_on_source_change && (shas[1] != @request.head_sha)
212
+ logger.info 'Re-running due to new commit in pull request.'
213
+ return true
214
+ elsif self.rerun_on_target_change && (shas[2] != @request.target_head_sha)
215
+ logger.info 'Re-running due to new commit in target branch.'
216
+ return true
217
+ end
218
+ else
219
+ # If there are no SHAs yet, it has to be a new request.
220
+ logger.info 'New pull request detected, run needed.'
197
221
  return true
198
222
  end
199
- else
200
- # If there are no comments yet, it has to be a new request.
201
- @log.info 'New pull request detected, run needed.'
202
- return true
203
223
  end
204
- @log.info "Not running for request ##{@request.id}."
224
+
225
+ logger.info "Pull request comes from a fork." if @request.from_fork
226
+ logger.info "Not running for request ##{@request.id}."
205
227
  false
206
228
  end
207
229
 
208
- def switch_branch_to_merged_state(hard = true)
230
+ def switch_branch_to_merged_state
209
231
  # Fetch the merge-commit for the pull request.
210
232
  # NOTE: This commit is automatically created by 'GitHub Merge Button'.
211
- # FIXME: Use cheetah to pipe to @log.debug instead of that /dev/null hack.
233
+ # FIXME: Use cheetah to pipe to logger.debug instead of that /dev/null hack.
212
234
  `git fetch origin refs/pull/#{@request.id}/merge: &> /dev/null`
213
- `git checkout FETCH_HEAD &> /dev/null`
214
- unless ($? && $?.exitstatus == 0)
215
- @log.error 'Unable to switch to merge branch.'
216
- hard ? abort : false
235
+ _output, status = Open3.capture2 'git checkout FETCH_HEAD &> /dev/null'
236
+ if status != 0
237
+ logger.error 'Unable to switch to merge branch.'
238
+ return false
217
239
  end
218
240
  true
219
241
  end
220
242
 
221
243
  def switch_branch_back
222
- # FIXME: Use cheetah to pipe to @log.debug instead of that /dev/null hack.
223
- @log.info 'Switching back to original branch.'
244
+ # FIXME: Use cheetah to pipe to logger.debug instead of that /dev/null hack.
245
+ logger.info 'Switching back to original branch.'
224
246
  # FIXME: For branches other than master, remember the original branch.
225
247
  `git checkout master &> /dev/null`
226
248
  # Clean up potential remains and run garbage collector.
@@ -242,33 +264,60 @@ class Prophet
242
264
  end
243
265
 
244
266
  def comment_on_github
267
+ return if self.disable_comments
268
+
245
269
  # Determine comment message.
246
270
  message = if self.success
247
- @log.info 'Successful run.'
248
- self.comment_success + "\n( Success: "
271
+ logger.info 'Successful run.'
272
+ self.comment_success
249
273
  else
250
- @log.info 'Failing run.'
251
- self.comment_failure + "\n( Failure: "
274
+ logger.info 'Failing run.'
275
+ self.comment_failure
252
276
  end
253
- message += "Merged #{@request.head_sha} into #{@request.target_head_sha} )"
277
+ message += status_string
254
278
  if self.reuse_comments && old_comment_success? == self.success
255
279
  # Replace existing comment's body with the correct connection.
256
- @log.info "Updating existing #{notion(self.success)} comment."
280
+ logger.info "Updating existing #{notion(self.success)} comment."
257
281
  call_github(self.success).update_comment(@project, @request.comment.id, message)
258
282
  else
259
283
  if @request.comment
260
- @log.info "Deleting existing #{notion(!self.success)} comment."
284
+ logger.info "Deleting existing #{notion(!self.success)} comment."
261
285
  # Delete old comment with correct connection (if @request.comment exists).
262
286
  call_github(!self.success).delete_comment(@project, @request.comment.id)
263
287
  end
264
288
  # Create new comment with correct connection.
265
- @log.info "Adding new #{notion(self.success)} comment."
289
+ logger.info "Adding new #{notion(self.success)} comment."
266
290
  call_github(self.success).add_comment(@project, @request.id, message)
267
291
  end
268
292
  end
269
293
 
294
+ def status_string
295
+ case self.success
296
+ when true
297
+ " (Merged #{@request.head_sha} into #{@request.target_head_sha})"
298
+ when false
299
+ " (Merged #{@request.head_sha} into #{@request.target_head_sha})"
300
+ else
301
+ ""
302
+ end
303
+ end
304
+
305
+ def create_status(state, description)
306
+ logger.info "Setting status '#{state}': '#{description}'"
307
+ @github.create_status(
308
+ @project,
309
+ @request.head_sha,
310
+ state,
311
+ {
312
+ "description" => description,
313
+ "context" => status_context,
314
+ "target_url" => self.status_target_url
315
+ }
316
+ )
317
+ end
318
+
270
319
  def set_status_on_github
271
- @log.info 'Updating status on GitHub.'
320
+ logger.info 'Updating status on GitHub.'
272
321
  case self.success
273
322
  when true
274
323
  state_symbol = :success
@@ -280,12 +329,7 @@ class Prophet
280
329
  state_symbol = :pending
281
330
  state_message = self.status_pending
282
331
  end
283
- @github.post(
284
- "repos/#{@project}/statuses/#{@request.head_sha}", {
285
- :state => state_symbol,
286
- :description => state_message
287
- }
288
- )
332
+ create_status(state_symbol, state_message + status_string)
289
333
  end
290
334
 
291
335
  def notion(success)
@@ -4,12 +4,16 @@ class PullRequest
4
4
  :content,
5
5
  :comment,
6
6
  :head_sha,
7
- :target_head_sha
7
+ :target_head_sha,
8
+ :from_fork,
9
+ :wip
8
10
 
9
11
  def initialize(content)
10
12
  @content = content
11
13
  @id = content.number
12
14
  @head_sha = content.head.sha
15
+ @from_fork = content.head.repo.fork
16
+ @wip = content.labels.map(&:name).map(&:downcase).include? 'wip'
13
17
  end
14
18
 
15
19
  end
@@ -3,6 +3,6 @@ require 'rails'
3
3
 
4
4
  class Railtie < Rails::Railtie
5
5
  rake_tasks do
6
- load 'tasks/prophet.rake.rake'
6
+ load 'tasks/prophet.rake'
7
7
  end
8
8
  end
metadata CHANGED
@@ -1,59 +1,59 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prophet
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.1
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dominik Bamberger
8
8
  - Thomas Schmidt
9
9
  - Jordi Massaguer Pla
10
- autorequire:
10
+ autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2014-07-01 00:00:00.000000000 Z
13
+ date: 2020-09-28 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: faraday_middleware
17
17
  requirement: !ruby/object:Gem::Requirement
18
18
  requirements:
19
- - - '='
19
+ - - "~>"
20
20
  - !ruby/object:Gem::Version
21
- version: 0.9.0
21
+ version: 0.11.0
22
22
  type: :runtime
23
23
  prerelease: false
24
24
  version_requirements: !ruby/object:Gem::Requirement
25
25
  requirements:
26
- - - '='
26
+ - - "~>"
27
27
  - !ruby/object:Gem::Version
28
- version: 0.9.0
28
+ version: 0.11.0
29
29
  - !ruby/object:Gem::Dependency
30
30
  name: faraday
31
31
  requirement: !ruby/object:Gem::Requirement
32
32
  requirements:
33
- - - '='
33
+ - - "~>"
34
34
  - !ruby/object:Gem::Version
35
- version: 0.8.8
35
+ version: 0.11.0
36
36
  type: :runtime
37
37
  prerelease: false
38
38
  version_requirements: !ruby/object:Gem::Requirement
39
39
  requirements:
40
- - - '='
40
+ - - "~>"
41
41
  - !ruby/object:Gem::Version
42
- version: 0.8.8
42
+ version: 0.11.0
43
43
  - !ruby/object:Gem::Dependency
44
44
  name: octokit
45
45
  requirement: !ruby/object:Gem::Requirement
46
46
  requirements:
47
- - - '='
47
+ - - "~>"
48
48
  - !ruby/object:Gem::Version
49
- version: 1.25.0
49
+ version: '4.0'
50
50
  type: :runtime
51
51
  prerelease: false
52
52
  version_requirements: !ruby/object:Gem::Requirement
53
53
  requirements:
54
- - - '='
54
+ - - "~>"
55
55
  - !ruby/object:Gem::Version
56
- version: 1.25.0
56
+ version: '4.0'
57
57
  description: Prophet runs custom code (i.e. your project's test suite) on open pull
58
58
  requests on GitHub. Afterwards it posts the result as a comment to the respective
59
59
  request. This should give you an outlook on the future state of your repository
@@ -73,26 +73,24 @@ homepage: http://github.com/b4mboo/prophet
73
73
  licenses:
74
74
  - MIT
75
75
  metadata: {}
76
- post_install_message:
76
+ post_install_message:
77
77
  rdoc_options: []
78
78
  require_paths:
79
79
  - lib
80
80
  required_ruby_version: !ruby/object:Gem::Requirement
81
81
  requirements:
82
- - - '>='
82
+ - - ">="
83
83
  - !ruby/object:Gem::Version
84
84
  version: '0'
85
85
  required_rubygems_version: !ruby/object:Gem::Requirement
86
86
  requirements:
87
- - - '>='
87
+ - - ">="
88
88
  - !ruby/object:Gem::Version
89
89
  version: '0'
90
90
  requirements: []
91
- rubyforge_project:
92
- rubygems_version: 2.2.2
93
- signing_key:
91
+ rubygems_version: 3.1.4
92
+ signing_key:
94
93
  specification_version: 4
95
94
  summary: An easy way to loop through open pull requests and run code onthe merged
96
95
  branch.
97
96
  test_files: []
98
- has_rdoc: