prophet 1.6.1 → 2.2.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 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: