prophet 1.5.4
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +20 -0
- data/lib/prophet.rb +5 -0
- data/lib/prophet/prophet.rb +310 -0
- data/lib/prophet/pull_request.rb +15 -0
- data/lib/prophet/railtie.rb +8 -0
- data/lib/tasks/prophet.rake +4 -0
- metadata +88 -0
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,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
|
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: []
|