pullermann 1.0.0

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
+
@@ -0,0 +1,203 @@
1
+ class Pullermann
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
+ :test_block,
11
+ :log_level
12
+
13
+ # Allow configuration blocks being passed to Pullermann.
14
+ def self.setup
15
+ yield main_instance
16
+ end
17
+
18
+ def test_preparation(&block)
19
+ self.prepare_block = block
20
+ end
21
+
22
+ def test_execution(&block)
23
+ self.test_block = block
24
+ end
25
+
26
+ # The main Pullermann task. Call this to start testing.
27
+ def self.run
28
+ main_instance.run
29
+ end
30
+
31
+ def run
32
+ # Populate variables and setup environment.
33
+ configure
34
+ # Loop through all 'open' pull requests.
35
+ pull_requests.each do |request|
36
+ @request_id = request['number']
37
+ # Jump to next iteration if source and/or target haven't change since last run.
38
+ next unless test_run_necessary?
39
+ # GitHub always creates a merge commit for its 'Merge Button'.
40
+ switch_branch_to_merged_state
41
+ # Prepare project and CI (e.g. Jenkins) for the test run.
42
+ self.prepare_block.call
43
+ # Run specified tests for the project.
44
+ # NOTE: Either ensure the last call in that block runs your tests
45
+ # or manually set @result to a boolean inside this block.
46
+ self.test_block.call
47
+ # Unless already set, the success/failure is determined by the last
48
+ # command's return code.
49
+ @result ||= $? == 0
50
+ # We need to switch back to the original branch in case we need to test
51
+ # more pull requests.
52
+ switch_branch_back
53
+ comment_on_github
54
+ end
55
+ end
56
+
57
+
58
+ private
59
+
60
+ # Remember the one instance we setup in our application and want to run.
61
+ def self.main_instance
62
+ @main_instance ||= Pullermann.new
63
+ end
64
+
65
+ def configure
66
+ @log = Logger.new(STDOUT)
67
+ @log.level = self.log_level || Logger::INFO
68
+ # Set default fall back values for options that aren't set.
69
+ self.username ||= git_config['github.login']
70
+ self.password ||= git_config['github.password']
71
+ self.username_fail ||= self.username
72
+ self.password_fail ||= self.password
73
+ self.rerun_on_source_change = true unless self.rerun_on_source_change == false
74
+ self.rerun_on_target_change = true unless self.rerun_on_target_change == false
75
+ # Find environment (tasks, project, ...).
76
+ @prepare_block ||= lambda {}
77
+ @test_block ||= lambda { `rake test` }
78
+ connect_to_github
79
+ end
80
+
81
+ def connect_to_github(user = self.username, pass = self.password)
82
+ @github = Octokit::Client.new(
83
+ :login => user,
84
+ :password => pass
85
+ )
86
+ # Check user login to GitHub.
87
+ @github.login
88
+ @log.info "Successfully logged into GitHub (API v#{@github.api_version}) with user '#{user}'."
89
+ # Ensure the user has access to desired project.
90
+ @project = /:(.*)\.git/.match(git_config['remote.origin.url'])[1]
91
+ begin
92
+ @github.repo @project
93
+ @log.info "Successfully accessed GitHub project '#{@project}'"
94
+ rescue Octokit::Unauthorized => e
95
+ @log.error "Unable to access GitHub project with user '#{user}':\n#{e.message}"
96
+ abort
97
+ end
98
+ end
99
+
100
+ def pull_requests
101
+ pulls = @github.pulls @project, 'open'
102
+ @log.info "Found #{pulls.size > 0 ? pulls.size : 'no'} open pull requests in '#{@project}'."
103
+ pulls
104
+ end
105
+
106
+ # Test runs are necessary if:
107
+ # - the pull request hasn't been tested before.
108
+ # - the pull request has been updated since the last run.
109
+ # - the target (i.e. master) has been updated since the last run.
110
+ def test_run_necessary?
111
+ pull_request = @github.pull_request @project, @request_id
112
+ @log.info "Checking pull request ##{@request_id}: #{pull_request.title}"
113
+ # If it's not mergeable, there is no point in going on.
114
+ unless pull_request.mergeable
115
+ @log.info 'Pull request not auto-mergeable, skipping... '
116
+ return false
117
+ end
118
+ comments = @github.issue_comments(@project, @request_id)
119
+ comments = comments.select{ |c| [username, username_fail].include?(c.user.login) }.reverse
120
+ if comments.empty?
121
+ # If there are no comments yet, it has to be a new request.
122
+ @log.info 'New pull request detected, test run needed.'
123
+ return true
124
+ else
125
+ # Compare current sha ids of target and source branch with those from the last test run.
126
+ @target_head_sha ||= @github.commits(@project).first.sha
127
+ @pull_head_sha = pull_request.head.sha
128
+ # Initialize shas to ensure it will live on after the 'each' block.
129
+ shas = nil
130
+ comments.each do |comment|
131
+ shas = /master sha# ([\w]+) ; pull sha# ([\w]+)/.match(comment.body)
132
+ break if shas && shas[1] && shas[2]
133
+ end
134
+ # We finally found the latest comment that includes the necessary information.
135
+ if shas && shas[1] && shas[2]
136
+ @log.info "Current target sha: '#{@target_head_sha}', pull sha: '#{@pull_head_sha}'."
137
+ @log.info "Last test run target sha: '#{shas[1]}', pull sha: '#{shas[2]}'."
138
+ if self.rerun_on_source_change && (shas[2] != @pull_head_sha)
139
+ @log.info 'Re-running test due to new commit in pull request.'
140
+ return true
141
+ elsif self.rerun_on_target_change && (shas[1] != @target_head_sha)
142
+ @log.info 'Re-running test due to new commit in target branch.'
143
+ return true
144
+ end
145
+ else
146
+ @log.info 'New pull request detected, test run needed.'
147
+ return true
148
+ end
149
+ end
150
+ @log.info "Not running tests for request ##{@request_id}."
151
+ false
152
+ end
153
+
154
+ def switch_branch_to_merged_state
155
+ # Fetch the merge-commit for the pull request.
156
+ # NOTE: This commit is automatically created by 'GitHub Merge Button'.
157
+ # FIXME: Use cheetah to pipe to @log.debug instead of that /dev/null hack.
158
+ `git fetch origin refs/pull/#{@request_id}/merge: &> /dev/null`
159
+ `git checkout FETCH_HEAD &> /dev/null`
160
+ unless $? == 0
161
+ @log.error 'Unable to switch to merge branch.'
162
+ abort
163
+ end
164
+
165
+ end
166
+
167
+ def switch_branch_back
168
+ # FIXME: Use cheetah to pipe to @log.debug instead of that /dev/null hack.
169
+ @log.info 'Switching back to original branch.'
170
+ # FIXME: For branches other than master, remember the original branch.
171
+ `git co master &> /dev/null`
172
+ end
173
+
174
+ # Output the result to a comment on the pull request on GitHub.
175
+ def comment_on_github
176
+ sha_string = "\n( master sha# #{@target_head_sha} ; pull sha# #{@pull_head_sha} )"
177
+ if @result
178
+ message = 'Well done! All tests are still passing after merging this pull request. '
179
+ else
180
+ unless self.username == self.username_fail
181
+ # Re-connect with username_fail and password_fail.
182
+ connect_to_github(self.username, self.password)
183
+ end
184
+ message = 'Unfortunately your tests are failing after merging this pull request. '
185
+ end
186
+ @github.add_comment(@project, @request_id, message + sha_string)
187
+ end
188
+
189
+ # Collect git config information in a Hash for easy access.
190
+ # Checks '~/.gitconfig' for credentials.
191
+ def git_config
192
+ unless @git_config
193
+ # Read @git_config from local git config.
194
+ @git_config = {}
195
+ `git config --list`.split("\n").each do |line|
196
+ key, value = line.split('=')
197
+ @git_config[key] = value
198
+ end
199
+ end
200
+ @git_config
201
+ end
202
+
203
+ end
@@ -0,0 +1,8 @@
1
+ require 'pullermann'
2
+ require 'rails'
3
+
4
+ class Railtie < Rails::Railtie
5
+ rake_tasks do
6
+ load 'tasks/pullermann.rake'
7
+ end
8
+ end
data/lib/pullermann.rb ADDED
@@ -0,0 +1,4 @@
1
+ require 'octokit'
2
+ require 'logger'
3
+ require 'pullermann/railtie' if defined?(Rails)
4
+ require 'pullermann/pullermann'
@@ -0,0 +1,4 @@
1
+ desc 'Test open pull requests'
2
+ task :pullermann => :environment do
3
+ Pullermann.run
4
+ end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: pullermann
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
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: 2012-06-14 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: Pullermann runs your project's test suite on open pull requests on GitHub.
49
+ Afterwards it posts the result as a comment to the respective request. That way
50
+ you know whether your tests are still going to pass if you accept the request and
51
+ merge the code.
52
+ email: bamboo@suse.com
53
+ executables: []
54
+ extensions: []
55
+ extra_rdoc_files: []
56
+ files:
57
+ - LICENSE
58
+ - lib/pullermann.rb
59
+ - lib/tasks/pullermann.rake
60
+ - lib/pullermann/pullermann.rb
61
+ - lib/pullermann/railtie.rb
62
+ homepage: http://github.com/b4mboo/pullermann
63
+ licenses: []
64
+ post_install_message:
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ none: false
70
+ requirements:
71
+ - - ! '>='
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ! '>='
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ requirements: []
81
+ rubyforge_project:
82
+ rubygems_version: 1.8.24
83
+ signing_key:
84
+ specification_version: 3
85
+ summary: An easy way to test pull requests.
86
+ test_files: []