prophet 1.5.4
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.
- 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: []
|