prophet 1.6.2 → 2.2.1

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: 3a03cdc7b74ef29b89a87fe997327cb088866998
4
- data.tar.gz: 0c30193560861e6d77f8c72e51033f023fcae629
2
+ SHA256:
3
+ metadata.gz: abc7fdf796527eb07bc79a4a33db0c14f8a2ff1257e6cc440f9f2849bce0aac9
4
+ data.tar.gz: 3fe67e31b766b0bbb65a4433fb98d60472e287d9bd3f153cf6decca5463d6afa
5
5
  SHA512:
6
- metadata.gz: 2f83eb11cc1a5d241598b66f05fa68e727cffa2a78ab154e7f09a06a02978c5f0fd894fda55920d54603a3f633d8384aac0c4d6000e56e09f943bc66f37a4b8a
7
- data.tar.gz: 6c15d03d14ec406e7c612763c6408b1c71b9a10a7bcd3f219e4cf5a9a2686d99325e7c56eb02297f88525865000f820a64569df47f58b9608d729afbbfc8c3bf
6
+ metadata.gz: 7fa6ebed72ea06d9d6864da904b6d2d63919c221639563ef1b30ea3af8dd136b6902a1519759afd8240a7b81df9ea945507b99da054b405204443977713d5896
7
+ data.tar.gz: 1d01a53dac853e473ad2edfcedbfd4373ba3ad0cd80ab217538966cb48207124a2fc49b3cc64d3bfb2e400b38f363305eaa5b2090ff449e5253ff6f51d26b852
@@ -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,46 +100,35 @@ 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
133
  # NOTE: All three variants should work:
131
134
  # 'ssh://git@github.com:user/project.git'
@@ -134,20 +137,20 @@ class Prophet
134
137
  @project ||= /github\.com[\/:](.*)\.git$/.match(git_config['remote.origin.url'])[1]
135
138
  begin
136
139
  github.repo @project
137
- @log.info "Successfully accessed GitHub project '#{@project}'"
140
+ logger.info "Successfully accessed GitHub project '#{@project}'"
138
141
  github
139
142
  rescue Octokit::Unauthorized => e
140
- @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}"
141
144
  abort
142
145
  end
143
146
  end
144
147
 
145
148
  def pull_requests
146
- request_list = @github.pulls @project, 'open'
149
+ request_list = @github.pulls @project, state: 'open'
147
150
  requests = request_list.collect do |request|
148
151
  PullRequest.new(@github.pull_request @project, request.number)
149
152
  end
150
- @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}'."
151
154
  requests
152
155
  end
153
156
 
@@ -155,73 +158,91 @@ class Prophet
155
158
  # - the pull request hasn't been used for a run before.
156
159
  # - the pull request has been updated since the last run.
157
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)
158
162
  def run_necessary?
159
- @log.info "Checking pull request ##{@request.id}: #{@request.content.title}"
160
- # Compare current sha ids of target and source branch with those from the last test run.
161
- @request.target_head_sha = @github.commits(@project).first.sha
162
- comments = @github.issue_comments(@project, @request.id)
163
- comments = comments.select { |c| [username, username_fail].include?(c.user.login) }.reverse
164
- # Initialize shas to ensure it will live on after the 'each' block.
165
- shas = nil
166
- @request.comment = nil
167
- comments.each do |comment|
168
- shas = /Merged ([\w]+) into ([\w]+)/.match(comment.body)
169
- if shas && shas[1] && shas[2]
170
- # Remember comment to be able to update or delete it later.
171
- @request.comment = comment
172
- 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)
173
172
  end
174
- end
175
- # If it's not mergeable, we need to delete all comments of former test runs.
176
- unless @request.content.mergeable
177
- # Sometimes GitHub doesn't have a proper boolean value stored.
178
- if @request.content.mergeable.nil? && switch_branch_to_merged_state(false)
179
- # Pull request is mergeable after all.
180
- 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
181
185
  else
182
- @log.info 'Pull request not auto-mergeable. Not running.'
183
- if @request.comment
184
- @log.info 'Deleting existing comment.'
185
- 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
186
198
  end
187
- return false
188
199
  end
189
- end
190
- if @request.comment
191
- @log.info "Current target sha: '#{@request.target_head_sha}', pull sha: '#{@request.head_sha}'."
192
- @log.info "Last test run target sha: '#{shas[2]}', pull sha: '#{shas[1]}'."
193
- if self.rerun_on_source_change && (shas[1] != @request.head_sha)
194
- @log.info 'Re-running due to new commit in pull request.'
195
- return true
196
- elsif self.rerun_on_target_change && (shas[2] != @request.target_head_sha)
197
- @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.'
198
221
  return true
199
222
  end
200
- else
201
- # If there are no comments yet, it has to be a new request.
202
- @log.info 'New pull request detected, run needed.'
203
- return true
204
223
  end
205
- @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}."
206
227
  false
207
228
  end
208
229
 
209
- def switch_branch_to_merged_state(hard = true)
230
+ def switch_branch_to_merged_state
210
231
  # Fetch the merge-commit for the pull request.
211
232
  # NOTE: This commit is automatically created by 'GitHub Merge Button'.
212
- # 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.
213
234
  `git fetch origin refs/pull/#{@request.id}/merge: &> /dev/null`
214
- `git checkout FETCH_HEAD &> /dev/null`
215
- unless ($? && $?.exitstatus == 0)
216
- @log.error 'Unable to switch to merge branch.'
217
- 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
218
239
  end
219
240
  true
220
241
  end
221
242
 
222
243
  def switch_branch_back
223
- # FIXME: Use cheetah to pipe to @log.debug instead of that /dev/null hack.
224
- @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.'
225
246
  # FIXME: For branches other than master, remember the original branch.
226
247
  `git checkout master &> /dev/null`
227
248
  # Clean up potential remains and run garbage collector.
@@ -243,33 +264,60 @@ class Prophet
243
264
  end
244
265
 
245
266
  def comment_on_github
267
+ return if self.disable_comments
268
+
246
269
  # Determine comment message.
247
270
  message = if self.success
248
- @log.info 'Successful run.'
249
- self.comment_success + "\n( Success: "
271
+ logger.info 'Successful run.'
272
+ self.comment_success
250
273
  else
251
- @log.info 'Failing run.'
252
- self.comment_failure + "\n( Failure: "
274
+ logger.info 'Failing run.'
275
+ self.comment_failure
253
276
  end
254
- message += "Merged #{@request.head_sha} into #{@request.target_head_sha} )"
277
+ message += status_string
255
278
  if self.reuse_comments && old_comment_success? == self.success
256
279
  # Replace existing comment's body with the correct connection.
257
- @log.info "Updating existing #{notion(self.success)} comment."
280
+ logger.info "Updating existing #{notion(self.success)} comment."
258
281
  call_github(self.success).update_comment(@project, @request.comment.id, message)
259
282
  else
260
283
  if @request.comment
261
- @log.info "Deleting existing #{notion(!self.success)} comment."
284
+ logger.info "Deleting existing #{notion(!self.success)} comment."
262
285
  # Delete old comment with correct connection (if @request.comment exists).
263
286
  call_github(!self.success).delete_comment(@project, @request.comment.id)
264
287
  end
265
288
  # Create new comment with correct connection.
266
- @log.info "Adding new #{notion(self.success)} comment."
289
+ logger.info "Adding new #{notion(self.success)} comment."
267
290
  call_github(self.success).add_comment(@project, @request.id, message)
268
291
  end
269
292
  end
270
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
+
271
319
  def set_status_on_github
272
- @log.info 'Updating status on GitHub.'
320
+ logger.info 'Updating status on GitHub.'
273
321
  case self.success
274
322
  when true
275
323
  state_symbol = :success
@@ -281,12 +329,7 @@ class Prophet
281
329
  state_symbol = :pending
282
330
  state_message = self.status_pending
283
331
  end
284
- @github.post(
285
- "repos/#{@project}/statuses/#{@request.head_sha}", {
286
- :state => state_symbol,
287
- :description => state_message
288
- }
289
- )
332
+ create_status(state_symbol, state_message + status_string)
290
333
  end
291
334
 
292
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.attrs[:repo].attrs[:fork] if content.head.attrs[:repo]
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,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: prophet
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.6.2
4
+ version: 2.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dominik Bamberger
@@ -10,50 +10,50 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2014-07-29 00:00:00.000000000 Z
13
+ date: 2021-03-17 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
@@ -89,7 +89,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
89
89
  version: '0'
90
90
  requirements: []
91
91
  rubyforge_project:
92
- rubygems_version: 2.2.2
92
+ rubygems_version: 2.7.6.2
93
93
  signing_key:
94
94
  specification_version: 4
95
95
  summary: An easy way to loop through open pull requests and run code onthe merged