class Dependabot::PullRequestCreator::Github
rubocop:disable Metrics/ClassLength
Attributes
assignees[R]
base_commit[R]
branch_name[R]
commit_message[R]
credentials[R]
custom_headers[R]
files[R]
labeler[R]
milestone[R]
pr_description[R]
pr_name[R]
reviewers[R]
signature_key[R]
source[R]
Public Class Methods
new(source:, branch_name:, base_commit:, credentials:, files:, commit_message:, pr_description:, pr_name:, author_details:, signature_key:, custom_headers:, labeler:, reviewers:, assignees:, milestone:, require_up_to_date_base:)
click to toggle source
# File lib/dependabot/pull_request_creator/github.rb, line 17 def initialize(source:, branch_name:, base_commit:, credentials:, files:, commit_message:, pr_description:, pr_name:, author_details:, signature_key:, custom_headers:, labeler:, reviewers:, assignees:, milestone:, require_up_to_date_base:) @source = source @branch_name = branch_name @base_commit = base_commit @credentials = credentials @files = files @commit_message = commit_message @pr_description = pr_description @pr_name = pr_name @author_details = author_details @signature_key = signature_key @custom_headers = custom_headers @labeler = labeler @reviewers = reviewers @assignees = assignees @milestone = milestone @require_up_to_date_base = require_up_to_date_base end
Public Instance Methods
create()
click to toggle source
# File lib/dependabot/pull_request_creator/github.rb, line 40 def create return if branch_exists?(branch_name) && unmerged_pull_request_exists? return if require_up_to_date_base? && !base_commit_is_up_to_date? create_annotated_pull_request rescue AnnotationError, Octokit::Error => e handle_error(e) end
Private Instance Methods
add_assignees_to_pull_request(pull_request)
click to toggle source
# File lib/dependabot/pull_request_creator/github.rb, line 328 def add_assignees_to_pull_request(pull_request) github_client_for_source.add_assignees( source.repo, pull_request.number, assignees ) rescue Octokit::NotFound # This can happen if a passed assignee login is now an org account nil end
add_milestone_to_pull_request(pull_request)
click to toggle source
# File lib/dependabot/pull_request_creator/github.rb, line 339 def add_milestone_to_pull_request(pull_request) github_client_for_source.update_issue( source.repo, pull_request.number, milestone: milestone ) rescue Octokit::UnprocessableEntity => e raise unless e.message.include?("code: invalid") end
add_reviewers_to_pull_request(pull_request)
click to toggle source
# File lib/dependabot/pull_request_creator/github.rb, line 268 def add_reviewers_to_pull_request(pull_request) reviewers_hash = reviewers.keys.map { |k| [k.to_sym, reviewers[k]] }.to_h github_client_for_source.request_pull_request_review( source.repo, pull_request.number, reviewers: reviewers_hash[:reviewers] || [], team_reviewers: reviewers_hash[:team_reviewers] || [] ) rescue Octokit::UnprocessableEntity => e # Special case GitHub bug for team reviewers return if e.message.include?("Could not resolve to a node") if invalid_reviewer?(e.message) comment_with_invalid_reviewer(pull_request, e.message) return end raise end
annotate_pull_request(pull_request)
click to toggle source
# File lib/dependabot/pull_request_creator/github.rb, line 261 def annotate_pull_request(pull_request) labeler.label_pull_request(pull_request.number) add_reviewers_to_pull_request(pull_request) if reviewers&.any? add_assignees_to_pull_request(pull_request) if assignees&.any? add_milestone_to_pull_request(pull_request) if milestone end
base_commit_is_up_to_date?()
click to toggle source
# File lib/dependabot/pull_request_creator/github.rb, line 103 def base_commit_is_up_to_date? git_metadata_fetcher.head_commit_for_ref(target_branch) == base_commit end
branch_exists?(name)
click to toggle source
rubocop:disable Metrics/PerceivedComplexity
# File lib/dependabot/pull_request_creator/github.rb, line 56 def branch_exists?(name) git_metadata_fetcher.ref_names.include?(name) rescue Dependabot::GitDependenciesNotReachable => e raise e.cause if e.cause&.message&.include?("is disabled") raise e.cause if e.cause.is_a?(Octokit::Unauthorized) raise(RepoNotFound, source.url) unless repo_exists? retrying ||= false msg = "Unexpected git error!\n\n#{e.cause&.class}: #{e.cause&.message}" raise msg if retrying retrying = true retry end
comment_with_invalid_reviewer(pull_request, message)
click to toggle source
# File lib/dependabot/pull_request_creator/github.rb, line 298 def comment_with_invalid_reviewer(pull_request, message) reviewers_hash = reviewers.keys.map { |k| [k.to_sym, reviewers[k]] }.to_h reviewers = [] reviewers += reviewers_hash[:reviewers] || [] reviewers += (reviewers_hash[:team_reviewers] || []). map { |rv| "#{source.repo.split('/').first}/#{rv}" } reviewers_string = if reviewers.count == 1 "`@#{reviewers.first}`" else names = reviewers.map { |rv| "`@#{rv}`" } "#{names[0..-2].join(', ')} and #{names[-1]}" end msg = "Dependabot tried to add #{reviewers_string} as " msg += reviewers.count > 1 ? "reviewers" : "a reviewer" msg += " to this PR, but received the following error from GitHub:\n\n"\ "```\n" \ "#{message}\n"\ "```" github_client_for_source.add_comment( source.repo, pull_request.number, msg ) end
commit_options(tree)
click to toggle source
# File lib/dependabot/pull_request_creator/github.rb, line 159 def commit_options(tree) options = author_details&.any? ? { author: author_details } : {} if options[:author]&.any? && signature_key options[:author][:date] = Time.now.utc.iso8601 options[:signature] = commit_signature(tree, options[:author]) end options end
commit_signature(tree, author_details_with_date)
click to toggle source
# File lib/dependabot/pull_request_creator/github.rb, line 400 def commit_signature(tree, author_details_with_date) CommitSigner.new( author_details: author_details_with_date, commit_message: commit_message, tree_sha: tree.sha, parent_sha: base_commit, signature_key: signature_key ).signature end
create_annotated_pull_request()
click to toggle source
# File lib/dependabot/pull_request_creator/github.rb, line 107 def create_annotated_pull_request commit = create_commit branch = create_or_update_branch(commit) return unless branch pull_request = create_pull_request return unless pull_request begin annotate_pull_request(pull_request) rescue StandardError => e raise AnnotationError.new(e, pull_request) end pull_request end
create_branch(commit)
click to toggle source
# File lib/dependabot/pull_request_creator/github.rb, line 228 def create_branch(commit) ref = "refs/heads/#{branch_name}" begin branch = github_client_for_source.create_ref(source.repo, ref, commit.sha) @branch_name = ref.gsub(%r{^refs/heads/}, "") branch rescue Octokit::UnprocessableEntity => e # Return quietly in the case of a race return nil if e.message.match?(/Reference already exists/i) retrying_branch_creation ||= false raise if retrying_branch_creation retrying_branch_creation = true # Branch creation will fail if a branch called `dependabot` already # exists, since git won't be able to create a dir with the same name ref = "refs/heads/#{SecureRandom.hex[0..3] + branch_name}" retry end end
create_commit()
click to toggle source
# File lib/dependabot/pull_request_creator/github.rb, line 131 def create_commit tree = create_tree begin github_client_for_source.create_commit( source.repo, commit_message, tree.sha, base_commit, commit_options(tree) ) rescue Octokit::UnprocessableEntity => e raise unless e.message == "Tree SHA does not exist" # Sometimes a race condition on GitHub's side means we get an error # here. No harm in retrying if we do. raise_or_increment_retry_counter(counter: @commit_creation, limit: 3) sleep(rand(1..1.99)) retry end rescue Octokit::UnprocessableEntity => e raise unless e.message == "Tree SHA does not exist" raise_or_increment_retry_counter(counter: @tree_creation, limit: 1) sleep(rand(1..1.99)) retry end
create_or_update_branch(commit)
click to toggle source
# File lib/dependabot/pull_request_creator/github.rb, line 207 def create_or_update_branch(commit) if branch_exists?(branch_name) update_branch(commit) else create_branch(commit) end rescue Octokit::UnprocessableEntity => e raise unless e.message.include?("Reference update failed //") # A race condition may cause GitHub to fail here, in which case we retry retry_count ||= 0 retry_count += 1 if retry_count > 10 raise "Repeatedly failed to create or update branch #{branch_name} "\ "with commit #{commit.sha}." end sleep(rand(1..1.99)) retry end
create_pull_request()
click to toggle source
# File lib/dependabot/pull_request_creator/github.rb, line 349 def create_pull_request github_client_for_source.create_pull_request( source.repo, target_branch, branch_name, pr_name, pr_description, headers: custom_headers || {} ) rescue Octokit::UnprocessableEntity => e return handle_pr_creation_error(e) if e.message.include? "Error summary" # Sometimes PR creation fails with no details (presumably because the # details are internal). It doesn't hurt to retry in these cases, in # case the cause is a race. retrying_pr_creation ||= false raise if retrying_pr_creation retrying_pr_creation = true retry end
create_tree()
click to toggle source
# File lib/dependabot/pull_request_creator/github.rb, line 170 def create_tree file_trees = files.map do |file| if file.type == "submodule" { path: file.path.sub(%r{^/}, ""), mode: "160000", type: "commit", sha: file.content } else content = if file.operation == Dependabot::DependencyFile::Operation::DELETE { sha: nil } elsif file.binary? sha = github_client_for_source.create_blob( source.repo, file.content, "base64" ) { sha: sha } else { content: file.content } end { path: (file.symlink_target || file.path).sub(%r{^/}, ""), mode: "100644", type: "blob" }.merge(content) end end github_client_for_source.create_tree( source.repo, file_trees, base_tree: base_commit ) end
default_branch()
click to toggle source
# File lib/dependabot/pull_request_creator/github.rb, line 387 def default_branch @default_branch ||= github_client_for_source.repository(source.repo).default_branch end
git_metadata_fetcher()
click to toggle source
# File lib/dependabot/pull_request_creator/github.rb, line 392 def git_metadata_fetcher @git_metadata_fetcher ||= GitMetadataFetcher.new( url: source.url, credentials: credentials ) end
github_client_for_source()
click to toggle source
# File lib/dependabot/pull_request_creator/github.rb, line 416 def github_client_for_source @github_client_for_source ||= Dependabot::Clients::GithubWithRetries.for_source( source: source, credentials: credentials ) end
handle_error(err)
click to toggle source
# File lib/dependabot/pull_request_creator/github.rb, line 424 def handle_error(err) cause = case err when AnnotationError err.cause else err end case cause when Octokit::Forbidden if err.message.include?("disabled") raise_custom_error err, RepoDisabled, err.message elsif err.message.include?("archived") raise_custom_error err, RepoArchived, err.message end raise err when Octokit::NotFound raise err if repo_exists? raise_custom_error err, RepoNotFound, err.message when Octokit::UnprocessableEntity raise_custom_error err, NoHistoryInCommon, err.message if err.message.include?("no history in common") raise err else raise err end end
handle_pr_creation_error(error)
click to toggle source
# File lib/dependabot/pull_request_creator/github.rb, line 371 def handle_pr_creation_error(error) # Ignore races that we lose return if error.message.include?("pull request already exists") # Ignore cases where the target branch has been deleted return if error.message.include?("field: base") && source.branch && !branch_exists?(source.branch) raise end
invalid_reviewer?(message)
click to toggle source
# File lib/dependabot/pull_request_creator/github.rb, line 290 def invalid_reviewer?(message) return true if message.include?("Could not resolve to a node") return true if message.include?("not a collaborator") return true if message.include?("Could not add requested reviewers") false end
pull_requests_for_branch()
click to toggle source
# File lib/dependabot/pull_request_creator/github.rb, line 77 def pull_requests_for_branch @pull_requests_for_branch ||= begin github_client_for_source.pull_requests( source.repo, head: "#{source.repo.split('/').first}:#{branch_name}", state: "all" ) rescue Octokit::InternalServerError # A GitHub bug sometimes means adding `state: all` causes problems. # In that case, fall back to making two separate requests. open_prs = github_client_for_source.pull_requests( source.repo, head: "#{source.repo.split('/').first}:#{branch_name}", state: "open" ) closed_prs = github_client_for_source.pull_requests( source.repo, head: "#{source.repo.split('/').first}:#{branch_name}", state: "closed" ) [*open_prs, *closed_prs] end end
raise_custom_error(base_err, type, message)
click to toggle source
# File lib/dependabot/pull_request_creator/github.rb, line 454 def raise_custom_error(base_err, type, message) case base_err when AnnotationError raise AnnotationError.new( type.new(message), base_err.pull_request ) else raise type, message end end
raise_or_increment_retry_counter(counter:, limit:)
click to toggle source
# File lib/dependabot/pull_request_creator/github.rb, line 410 def raise_or_increment_retry_counter(counter:, limit:) counter ||= 0 counter += 1 raise if counter > limit end
repo_exists?()
click to toggle source
# File lib/dependabot/pull_request_creator/github.rb, line 124 def repo_exists? github_client_for_source.repo(source.repo) true rescue Octokit::NotFound false end
require_up_to_date_base?()
click to toggle source
# File lib/dependabot/pull_request_creator/github.rb, line 51 def require_up_to_date_base? @require_up_to_date_base end
target_branch()
click to toggle source
# File lib/dependabot/pull_request_creator/github.rb, line 383 def target_branch source.branch || default_branch end
unmerged_pull_request_exists?()
click to toggle source
rubocop:enable Metrics/PerceivedComplexity
# File lib/dependabot/pull_request_creator/github.rb, line 73 def unmerged_pull_request_exists? pull_requests_for_branch.reject(&:merged).any? end
update_branch(commit)
click to toggle source
# File lib/dependabot/pull_request_creator/github.rb, line 252 def update_branch(commit) github_client_for_source.update_ref( source.repo, "heads/#{branch_name}", commit.sha, true ) end