prophet 1.6.2 → 2.2.1

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: 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