class Prophet
Attributes
access_token_fail[RW]
access_token_pass[RW]
comment_failure[RW]
comment_success[RW]
disable_comments[RW]
exec_block[RW]
logger[RW]
prepare_block[RW]
rerun_on_source_change[RW]
rerun_on_target_change[RW]
reuse_comments[RW]
status_context[RW]
status_failure[RW]
status_pending[RW]
status_success[RW]
status_target_url[RW]
success[RW]
username_fail[RW]
username_pass[RW]
Public Class Methods
run()
click to toggle source
The main Prophet
task. Call this to run your code.
# File lib/prophet/prophet.rb, line 41 def self.run main_instance.run end
setup() { |main_instance| ... }
click to toggle source
Allow configuration blocks being passed to Prophet
. See the README.md for examples on how to call this method.
# File lib/prophet/prophet.rb, line 28 def self.setup yield main_instance end
Private Class Methods
main_instance()
click to toggle source
Remember the one instance we setup in our application and want to run.
# File lib/prophet/prophet.rb, line 98 def self.main_instance @main_instance ||= Prophet.new end
Public Instance Methods
execution(&block)
click to toggle source
# File lib/prophet/prophet.rb, line 36 def execution(&block) self.exec_block = block end
preparation(&block)
click to toggle source
# File lib/prophet/prophet.rb, line 32 def preparation(&block) self.prepare_block = block end
run()
click to toggle source
# File lib/prophet/prophet.rb, line 45 def run # Populate variables and setup environment. configure begin self.prepare_block.call rescue Exception => e logger.error "Preparation block raised an exception: #{e}" end # Loop through all 'open' pull requests. selected_requests = pull_requests.select do |request| @request = request # Jump to next iteration if source and/or target didn't change since last run. next unless run_necessary? set_status_on_github remove_comment unless self.reuse_comments true end # Run code on all selected requests. selected_requests.each do |request| @request = request logger.info "Running for request ##{@request.id}." # GitHub always creates a merge commit for its 'Merge Button'. # Prophet reuses that commit to run the code on it. if switch_branch_to_merged_state # Run specified code (i.e. tests) for the project. begin self.exec_block.call # Unless self.success has already been set (to true/false) manually, # the success/failure is determined by the last command's return code. self.success = ($CHILD_STATUS && $CHILD_STATUS.exitstatus == 0) if self.success.nil? rescue Exception => e logger.error "Execution block raised an exception: #{e}" self.success = false end switch_branch_back comment_on_github set_status_on_github end self.success = nil end end
Private Instance Methods
call_github(use_default_user = true)
click to toggle source
Determine which connection to GitHub should be used for the call.
# File lib/prophet/prophet.rb, line 340 def call_github(use_default_user = true) use_default_user ? @github : @github_fail end
comment_on_github()
click to toggle source
# File lib/prophet/prophet.rb, line 266 def comment_on_github return if self.disable_comments # Determine comment message. message = if self.success logger.info 'Successful run.' self.comment_success else logger.info 'Failing run.' self.comment_failure end message += status_string if self.reuse_comments && old_comment_success? == self.success # Replace existing comment's body with the correct connection. logger.info "Updating existing #{notion(self.success)} comment." call_github(self.success).update_comment(@project, @request.comment.id, message) else if @request.comment logger.info "Deleting existing #{notion(!self.success)} comment." # Delete old comment with correct connection (if @request.comment exists). call_github(!self.success).delete_comment(@project, @request.comment.id) end # Create new comment with correct connection. logger.info "Adding new #{notion(self.success)} comment." call_github(self.success).add_comment(@project, @request.id, message) end end
configure()
click to toggle source
# File lib/prophet/prophet.rb, line 102 def configure self.username_fail ||= self.username_pass self.access_token_fail ||= self.access_token_pass # Set default fall back values for options that aren't set. self.rerun_on_source_change = true if self.rerun_on_source_change.nil? self.rerun_on_target_change = true if self.rerun_on_target_change.nil? self.reuse_comments = false if self.reuse_comments.nil? self.disable_comments = false if self.disable_comments.nil? # Allow for custom messages. self.status_pending ||= 'Prophet is still running.' self.status_failure ||= 'Prophet reports failure.' self.status_success ||= 'Prophet reports success.' self.comment_failure ||= 'Prophet reports failure.' self.comment_success ||= 'Prophet reports success.' self.status_context ||= 'prophet/default' # Find environment (tasks, project, ...). self.prepare_block ||= lambda {} self.exec_block ||= lambda { `rake` } @github = connect_to_github(access_token: access_token_pass) @github_fail = connect_to_github(access_token: access_token_fail) end
connect_to_github(access_token:)
click to toggle source
# File lib/prophet/prophet.rb, line 125 def connect_to_github(access_token:) github = Octokit::Client.new(access_token: access_token) # Check user login to GitHub. github.login logger.info "Successfully logged into GitHub with user '#{github.user.login}'." # Ensure the user has access to desired project. # NOTE: All three variants should work: # 'ssh://git@github.com:user/project.git' # 'git@github.com:user/project.git' # 'https://github.com/user/project.git' @project ||= /github\.com[\/:](.*)\.git$/.match(git_config['remote.origin.url'])[1] begin github.repo @project logger.info "Successfully accessed GitHub project '#{@project}'" github rescue Octokit::Unauthorized => e logger.error "Unable to access GitHub project with user '#{github.user}':\n#{e.message}" abort end end
create_status(state, description)
click to toggle source
# File lib/prophet/prophet.rb, line 305 def create_status(state, description) logger.info "Setting status '#{state}': '#{description}'" @github.create_status( @project, @request.head_sha, state, { "description" => description, "context" => status_context, "target_url" => self.status_target_url } ) end
git_config()
click to toggle source
Collect git config information in a Hash for easy access. Checks '~/.gitconfig' for credentials.
# File lib/prophet/prophet.rb, line 346 def git_config unless @git_config @git_config = {} `git config --list`.split("\n").each do |line| key, value = line.split('=') @git_config[key] = value end end @git_config end
notion(success)
click to toggle source
# File lib/prophet/prophet.rb, line 335 def notion(success) success ? 'positive' : 'negative' end
old_comment_success?()
click to toggle source
# File lib/prophet/prophet.rb, line 252 def old_comment_success? return unless @request.comment # Analyze old comment to see whether it was a successful or a failing one. @request.comment.body.include? '( Success: ' end
pull_requests()
click to toggle source
# File lib/prophet/prophet.rb, line 148 def pull_requests request_list = @github.pulls @project, state: 'open' requests = request_list.collect do |request| PullRequest.new(@github.pull_request @project, request.number) end logger.info "Found #{requests.size > 0 ? requests.size : 'no'} open pull requests in '#{@project}'." requests end
remove_comment()
click to toggle source
# File lib/prophet/prophet.rb, line 258 def remove_comment if @request.comment # Remove old comment and reset variable. call_github(old_comment_success?).delete_comment(@project, @request.comment.id) @request.comment = nil end end
run_necessary?()
click to toggle source
(Re-)runs are necessary if:
-
the pull request hasn't been used for a run before.
-
the pull request has been updated since the last run.
-
the target (i.e. master) has been updated since the last run.
-
the pull request does not originate from a fork (to avoid malicious code execution on CI machines)
# File lib/prophet/prophet.rb, line 162 def run_necessary? logger.info "Checking pull request ##{@request.id}: #{@request.content.title}" unless @request.from_fork || @request.wip # Compare current sha ids of target and source branch with those from the last test run. @request.target_head_sha = @github.commits(@project).first.sha comments = @github.issue_comments(@project, @request.id) comments = comments.select { |c| [username_pass, username_fail].include?(c.user.login) }.reverse comments.each do |comment| @request.comment = comment if /Merged ([\w]+) into ([\w]+)/.match(comment.body) end statuses = @github.status(@project, @request.head_sha).statuses.select { |s| s.context == self.status_context } # Only run if it's mergeable. if @request.content.mergeable if statuses.empty? # If there is no status yet, it has to be a new request. logger.info 'New pull request detected, run needed.' return true elsif !self.disable_comments && !@request.comment logger.info 'Rerun forced.' return true end else # Sometimes GitHub doesn't have a proper boolean value stored. if @request.content.mergeable.nil? && switch_branch_to_merged_state # Pull request is mergeable after all. switch_branch_back else logger.info 'Pull request not auto-mergeable. Not running.' if @request.comment logger.info 'Deleting existing comment.' call_github(old_comment_success?).delete_comment(@project, @request.comment.id) end create_status(:error, "Pull request not auto-mergeable. Not running.") if statuses.first && statuses.first.state != 'error' return false end end # Initialize shas to ensure it will live on after the 'each' block. shas = nil statuses.each do |status| shas = /Merged ([\w]+) into ([\w]+)/.match(status.description) break if shas && shas[1] && shas[2] end if shas logger.info "Current target sha: '#{@request.target_head_sha}', pull sha: '#{@request.head_sha}'." logger.info "Last test run target sha: '#{shas[2]}', pull sha: '#{shas[1]}'." if self.rerun_on_source_change && (shas[1] != @request.head_sha) logger.info 'Re-running due to new commit in pull request.' return true elsif self.rerun_on_target_change && (shas[2] != @request.target_head_sha) logger.info 'Re-running due to new commit in target branch.' return true end else # If there are no SHAs yet, it has to be a new request. logger.info 'New pull request detected, run needed.' return true end end logger.info "Pull request comes from a fork." if @request.from_fork logger.info "Not running for request ##{@request.id}." false end
set_status_on_github()
click to toggle source
# File lib/prophet/prophet.rb, line 319 def set_status_on_github logger.info 'Updating status on GitHub.' case self.success when true state_symbol = :success state_message = self.status_success when false state_symbol = :failure state_message = self.status_failure else state_symbol = :pending state_message = self.status_pending end create_status(state_symbol, state_message + status_string) end
status_string()
click to toggle source
# File lib/prophet/prophet.rb, line 294 def status_string case self.success when true " (Merged #{@request.head_sha} into #{@request.target_head_sha})" when false " (Merged #{@request.head_sha} into #{@request.target_head_sha})" else "" end end
switch_branch_back()
click to toggle source
# File lib/prophet/prophet.rb, line 243 def switch_branch_back # FIXME: Use cheetah to pipe to logger.debug instead of that /dev/null hack. logger.info 'Switching back to original branch.' # FIXME: For branches other than master, remember the original branch. `git checkout master &> /dev/null` # Clean up potential remains and run garbage collector. `git gc &> /dev/null` end
switch_branch_to_merged_state()
click to toggle source
# File lib/prophet/prophet.rb, line 230 def switch_branch_to_merged_state # Fetch the merge-commit for the pull request. # NOTE: This commit is automatically created by 'GitHub Merge Button'. # FIXME: Use cheetah to pipe to logger.debug instead of that /dev/null hack. `git fetch origin refs/pull/#{@request.id}/merge: &> /dev/null` _output, status = Open3.capture2 'git checkout FETCH_HEAD &> /dev/null' if status != 0 logger.error 'Unable to switch to merge branch.' return false end true end