prophet 1.5.4

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (C) 2011 Dominik Bamberger bamboo@suse.com
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the "Software"), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7
+ of the Software, and to permit persons to whom the Software is furnished to do
8
+ so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
20
+
data/lib/prophet.rb ADDED
@@ -0,0 +1,5 @@
1
+ require 'octokit'
2
+ require 'logger'
3
+ require 'prophet/railtie' if defined?(Rails)
4
+ require 'prophet/pull_request'
5
+ require 'prophet/prophet'
@@ -0,0 +1,310 @@
1
+ class Prophet
2
+
3
+ attr_accessor :username,
4
+ :password,
5
+ :username_fail,
6
+ :password_fail,
7
+ :rerun_on_source_change,
8
+ :rerun_on_target_change,
9
+ :prepare_block,
10
+ :exec_block,
11
+ :logger,
12
+ :success,
13
+ :status_pending,
14
+ :status_failure,
15
+ :status_success,
16
+ :comment_failure,
17
+ :comment_success,
18
+ :reuse_comments
19
+
20
+ # Allow configuration blocks being passed to Prophet.
21
+ # See the README.md for examples on how to call this method.
22
+ def self.setup
23
+ yield main_instance
24
+ end
25
+
26
+ def preparation(&block)
27
+ self.prepare_block = block
28
+ end
29
+
30
+ def execution(&block)
31
+ self.exec_block = block
32
+ end
33
+
34
+ # The main Prophet task. Call this to run your code.
35
+ def self.run
36
+ main_instance.run
37
+ end
38
+
39
+ def run
40
+ # Populate variables and setup environment.
41
+ configure
42
+ begin
43
+ self.prepare_block.call
44
+ rescue Exception => e
45
+ @log.error "Preparation block raised an exception: #{e}"
46
+ end
47
+ # Loop through all 'open' pull requests.
48
+ selected_requests = pull_requests.select do |request|
49
+ @request = request
50
+ # Jump to next iteration if source and/or target didn't change since last run.
51
+ next unless run_necessary?
52
+ set_status_on_github
53
+ remove_comment unless self.reuse_comments
54
+ true
55
+ end
56
+ # Run code on all selected requests.
57
+ selected_requests.each do |request|
58
+ @request = request
59
+ @log.info "Running for request ##{@request.id}."
60
+ # GitHub always creates a merge commit for its 'Merge Button'.
61
+ # 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 manually,
67
+ # the success/failure is determined by the last command's return code.
68
+ self.success ||= ($? && $?.exitstatus == 0)
69
+ rescue Exception => e
70
+ @log.error "Execution block raised an exception: #{e}"
71
+ self.success = false
72
+ end
73
+ switch_branch_back
74
+ comment_on_github
75
+ set_status_on_github
76
+ self.success = nil
77
+ end
78
+ end
79
+
80
+
81
+ private
82
+
83
+ # Remember the one instance we setup in our application and want to run.
84
+ def self.main_instance
85
+ @main_instance ||= Prophet.new
86
+ end
87
+
88
+ 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
96
+ # 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
+ self.rerun_on_source_change = true if self.rerun_on_source_change.nil?
102
+ self.rerun_on_target_change = true if self.rerun_on_target_change.nil?
103
+ self.reuse_comments = true if self.reuse_comments.nil?
104
+ # Allow for custom messages.
105
+ self.status_pending ||= 'Prophet is still running.'
106
+ self.status_failure ||= 'Prophet reports failure.'
107
+ self.status_success ||= 'Prophet reports success.'
108
+ self.comment_failure ||= 'Prophet reports failure.'
109
+ self.comment_success ||= 'Prophet reports success.'
110
+ # Find environment (tasks, project, ...).
111
+ self.prepare_block ||= lambda {}
112
+ 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
119
+ end
120
+
121
+ def connect_to_github(user = self.username, pass = self.password)
122
+ github = Octokit::Client.new(
123
+ :login => user,
124
+ :password => pass
125
+ )
126
+ # Check user login to GitHub.
127
+ github.login
128
+ @log.info "Successfully logged into GitHub (API v#{github.api_version}) with user '#{user}'."
129
+ # Ensure the user has access to desired project.
130
+ @project ||= /:(.*)\.git/.match(git_config['remote.origin.url'])[1]
131
+ begin
132
+ github.repo @project
133
+ @log.info "Successfully accessed GitHub project '#{@project}'"
134
+ github
135
+ rescue Octokit::Unauthorized => e
136
+ @log.error "Unable to access GitHub project with user '#{user}':\n#{e.message}"
137
+ abort
138
+ end
139
+ end
140
+
141
+ def pull_requests
142
+ request_list = @github.pulls @project, 'open'
143
+ requests = request_list.collect do |request|
144
+ PullRequest.new(@github.pull_request @project, request.number)
145
+ end
146
+ @log.info "Found #{requests.size > 0 ? requests.size : 'no'} open pull requests in '#{@project}'."
147
+ requests
148
+ end
149
+
150
+ # (Re-)runs are necessary if:
151
+ # - the pull request hasn't been used for a run before.
152
+ # - the pull request has been updated since the last run.
153
+ # - the target (i.e. master) has been updated since the last run.
154
+ def run_necessary?
155
+ @log.info "Checking pull request ##{@request.id}: #{@request.content.title}"
156
+ # Compare current sha ids of target and source branch with those from the last test run.
157
+ @request.target_head_sha = @github.commits(@project).first.sha
158
+ comments = @github.issue_comments(@project, @request.id)
159
+ comments = comments.select { |c| [username, username_fail].include?(c.user.login) }.reverse
160
+ # Initialize shas to ensure it will live on after the 'each' block.
161
+ shas = nil
162
+ @request.comment = nil
163
+ comments.each do |comment|
164
+ shas = /Merged ([\w]+) into ([\w]+)/.match(comment.body)
165
+ if shas && shas[1] && shas[2]
166
+ # Remember comment to be able to update or delete it later.
167
+ @request.comment = comment
168
+ break
169
+ end
170
+ end
171
+ # If it's not mergeable, we need to delete all comments of former test runs.
172
+ unless @request.content.mergeable
173
+ # Sometimes GitHub doesn't have a proper boolean value stored.
174
+ if @request.content.mergeable.nil? && switch_branch_to_merged_state(false)
175
+ # Pull request is mergeable after all.
176
+ switch_branch_back
177
+ else
178
+ @log.info 'Pull request not auto-mergeable. Not running.'
179
+ if @request.comment
180
+ @log.info 'Deleting existing comment.'
181
+ call_github(old_comment_success?).delete_comment(@project, @request.comment.id)
182
+ end
183
+ return false
184
+ end
185
+ end
186
+ if @request.comment
187
+ @log.info "Current target sha: '#{@request.target_head_sha}', pull sha: '#{@request.head_sha}'."
188
+ @log.info "Last test run target sha: '#{shas[2]}', pull sha: '#{shas[1]}'."
189
+ if self.rerun_on_source_change && (shas[1] != @request.head_sha)
190
+ @log.info 'Re-running due to new commit in pull request.'
191
+ return true
192
+ elsif self.rerun_on_target_change && (shas[2] != @request.target_head_sha)
193
+ @log.info 'Re-running due to new commit in target branch.'
194
+ return true
195
+ end
196
+ else
197
+ # If there are no comments yet, it has to be a new request.
198
+ @log.info 'New pull request detected, run needed.'
199
+ return true
200
+ end
201
+ @log.info "Not running for request ##{@request.id}."
202
+ false
203
+ end
204
+
205
+ def switch_branch_to_merged_state(hard = true)
206
+ # Fetch the merge-commit for the pull request.
207
+ # NOTE: This commit is automatically created by 'GitHub Merge Button'.
208
+ # FIXME: Use cheetah to pipe to @log.debug instead of that /dev/null hack.
209
+ `git fetch origin refs/pull/#{@request.id}/merge: &> /dev/null`
210
+ `git checkout FETCH_HEAD &> /dev/null`
211
+ unless ($? && $?.exitstatus == 0)
212
+ @log.error 'Unable to switch to merge branch.'
213
+ hard ? abort : false
214
+ end
215
+ true
216
+ end
217
+
218
+ def switch_branch_back
219
+ # FIXME: Use cheetah to pipe to @log.debug instead of that /dev/null hack.
220
+ @log.info 'Switching back to original branch.'
221
+ # FIXME: For branches other than master, remember the original branch.
222
+ `git checkout master &> /dev/null`
223
+ # Clean up potential remains and run garbage collector.
224
+ `git gc &> /dev/null`
225
+ end
226
+
227
+ def old_comment_success?
228
+ return unless @request.comment
229
+ # Analyze old comment to see whether it was a successful or a failing one.
230
+ @request.comment.body.include? '( Success: '
231
+ end
232
+
233
+ def remove_comment
234
+ if @request.comment
235
+ # Remove old comment and reset variable.
236
+ call_github(old_comment_success?).delete_comment(@project, @request.comment.id)
237
+ @request.comment = nil
238
+ end
239
+ end
240
+
241
+ def comment_on_github
242
+ # Determine comment message.
243
+ message = if self.success
244
+ @log.info 'Successful run.'
245
+ self.comment_success + "\n( Success: "
246
+ else
247
+ @log.info 'Failing run.'
248
+ self.comment_failure + "\n( Failure: "
249
+ end
250
+ message += "Merged #{@request.head_sha} into #{@request.target_head_sha} )"
251
+ if self.reuse_comments && old_comment_success? == self.success
252
+ # Replace existing comment's body with the correct connection.
253
+ @log.info "Updating existing #{notion(self.success)} comment."
254
+ call_github(self.success).update_comment(@project, @request.comment.id, message)
255
+ else
256
+ if @request.comment
257
+ @log.info "Deleting existing #{notion(!self.success)} comment."
258
+ # Delete old comment with correct connection (if @request.comment exists).
259
+ call_github(!self.success).delete_comment(@project, @request.comment.id)
260
+ end
261
+ # Create new comment with correct connection.
262
+ @log.info "Adding new #{notion(self.success)} comment."
263
+ call_github(self.success).add_comment(@project, @request.id, message)
264
+ end
265
+ end
266
+
267
+ def set_status_on_github
268
+ @log.info 'Updating status on GitHub.'
269
+ case self.success
270
+ when true
271
+ state_symbol = :success
272
+ state_message = self.status_success
273
+ when false
274
+ state_symbol = :failure
275
+ state_message = self.status_failure
276
+ else
277
+ state_symbol = :pending
278
+ state_message = self.status_pending
279
+ end
280
+ @github.post(
281
+ "repos/#{@project}/statuses/#{@request.head_sha}", {
282
+ :state => state_symbol,
283
+ :description => state_message
284
+ }
285
+ )
286
+ end
287
+
288
+ def notion(success)
289
+ success ? 'positive' : 'negative'
290
+ end
291
+
292
+ # Determine which connection to GitHub should be used for the call.
293
+ def call_github(use_default_user = true)
294
+ use_default_user ? @github : @github_fail
295
+ end
296
+
297
+ # Collect git config information in a Hash for easy access.
298
+ # Checks '~/.gitconfig' for credentials.
299
+ def git_config
300
+ unless @git_config
301
+ @git_config = {}
302
+ `git config --list`.split("\n").each do |line|
303
+ key, value = line.split('=')
304
+ @git_config[key] = value
305
+ end
306
+ end
307
+ @git_config
308
+ end
309
+
310
+ end
@@ -0,0 +1,15 @@
1
+ class PullRequest
2
+
3
+ attr_accessor :id,
4
+ :content,
5
+ :comment,
6
+ :head_sha,
7
+ :target_head_sha
8
+
9
+ def initialize(content)
10
+ @content = content
11
+ @id = content.number
12
+ @head_sha = content.head.sha
13
+ end
14
+
15
+ end
@@ -0,0 +1,8 @@
1
+ require 'prophet'
2
+ require 'rails'
3
+
4
+ class Railtie < Rails::Railtie
5
+ rake_tasks do
6
+ load 'tasks/prophet.rake.rake'
7
+ end
8
+ end
@@ -0,0 +1,4 @@
1
+ desc 'Test open pull requests'
2
+ task :prophet => :environment do
3
+ Prophet.run
4
+ end
metadata ADDED
@@ -0,0 +1,88 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: prophet
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.5.4
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Dominik Bamberger
9
+ - Thomas Schmidt
10
+ - Jordi Massaguer Pla
11
+ autorequire:
12
+ bindir: bin
13
+ cert_chain: []
14
+ date: 2013-04-11 00:00:00.000000000 Z
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: octokit
18
+ requirement: !ruby/object:Gem::Requirement
19
+ none: false
20
+ requirements:
21
+ - - ! '>='
22
+ - !ruby/object:Gem::Version
23
+ version: '0'
24
+ type: :runtime
25
+ prerelease: false
26
+ version_requirements: !ruby/object:Gem::Requirement
27
+ none: false
28
+ requirements:
29
+ - - ! '>='
30
+ - !ruby/object:Gem::Version
31
+ version: '0'
32
+ - !ruby/object:Gem::Dependency
33
+ name: rspec
34
+ requirement: !ruby/object:Gem::Requirement
35
+ none: false
36
+ requirements:
37
+ - - ! '>='
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ type: :runtime
41
+ prerelease: false
42
+ version_requirements: !ruby/object:Gem::Requirement
43
+ none: false
44
+ requirements:
45
+ - - ! '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ description: Prophet runs custom code (i.e. your project's test suite) on open pull
49
+ requests on GitHub. Afterwards it posts the result as a comment to the respective
50
+ request. This should give you an outlook on the future state of your repository
51
+ in case you accept the request and merge the code.
52
+ email: bamboo@suse.com
53
+ executables: []
54
+ extensions: []
55
+ extra_rdoc_files: []
56
+ files:
57
+ - LICENSE
58
+ - lib/tasks/prophet.rake
59
+ - lib/prophet.rb
60
+ - lib/prophet/railtie.rb
61
+ - lib/prophet/pull_request.rb
62
+ - lib/prophet/prophet.rb
63
+ homepage: http://github.com/b4mboo/prophet
64
+ licenses: []
65
+ post_install_message:
66
+ rdoc_options: []
67
+ require_paths:
68
+ - lib
69
+ required_ruby_version: !ruby/object:Gem::Requirement
70
+ none: false
71
+ requirements:
72
+ - - ! '>='
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ none: false
77
+ requirements:
78
+ - - ! '>='
79
+ - !ruby/object:Gem::Version
80
+ version: '0'
81
+ requirements: []
82
+ rubyforge_project:
83
+ rubygems_version: 1.8.23
84
+ signing_key:
85
+ specification_version: 3
86
+ summary: An easy way to loop through open pull requests and run code onthe merged
87
+ branch.
88
+ test_files: []