pullermann 1.0.0

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