| |
| |
|
|
| require "octokit" |
| require "sorbet-runtime" |
|
|
| require "dependabot/clients/github_with_retries" |
| require "dependabot/pull_request_creator/commit_signer" |
| require "dependabot/pull_request_updater" |
|
|
| module Dependabot |
| class PullRequestUpdater |
| class Github |
| extend T::Sig |
|
|
| sig { returns(Dependabot::Source) } |
| attr_reader :source |
|
|
| sig { returns(T::Array[Dependabot::DependencyFile]) } |
| attr_reader :files |
|
|
| sig { returns(String) } |
| attr_reader :base_commit |
|
|
| sig { returns(String) } |
| attr_reader :old_commit |
|
|
| sig { returns(T::Array[Dependabot::Credential]) } |
| attr_reader :credentials |
|
|
| sig { returns(Integer) } |
| attr_reader :pull_request_number |
|
|
| sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) } |
| attr_reader :author_details |
|
|
| sig { returns(T.nilable(String)) } |
| attr_reader :signature_key |
|
|
| sig do |
| params( |
| source: Dependabot::Source, |
| base_commit: String, |
| old_commit: String, |
| files: T::Array[Dependabot::DependencyFile], |
| credentials: T::Array[Dependabot::Credential], |
| pull_request_number: Integer, |
| author_details: T.nilable(T::Hash[Symbol, T.untyped]), |
| signature_key: T.nilable(String) |
| ) |
| .void |
| end |
| def initialize( |
| source:, |
| base_commit:, |
| old_commit:, |
| files:, |
| credentials:, |
| pull_request_number:, |
| author_details: nil, |
| signature_key: nil |
| ) |
| @source = source |
| @base_commit = base_commit |
| @old_commit = old_commit |
| @files = files |
| @credentials = credentials |
| @pull_request_number = pull_request_number |
| @author_details = author_details |
| @signature_key = signature_key |
| end |
|
|
| sig { returns(T.nilable(Sawyer::Resource)) } |
| def update |
| return unless pull_request_exists? |
| return unless branch_exists?(pull_request.head.ref) |
|
|
| commit = create_commit |
| branch = update_branch(commit) |
| update_pull_request_target_branch |
| branch |
| end |
|
|
| private |
|
|
| sig { void } |
| def update_pull_request_target_branch |
| target_branch = source.branch || pull_request.base.repo.default_branch |
| return if target_branch == pull_request.base.ref |
|
|
| T.unsafe(github_client_for_source).update_pull_request( |
| source.repo, |
| pull_request_number, |
| base: target_branch |
| ) |
| rescue Octokit::UnprocessableEntity => e |
| handle_pr_update_error(e) |
| end |
|
|
| sig { params(error: Octokit::Error).void } |
| def handle_pr_update_error(error) |
| |
| return if error.message.match?(/closed pull request/i) |
|
|
| |
| return if error.message.include?("field: base") && |
| source.branch && |
| !branch_exists?(T.must(source.branch)) |
|
|
| raise error |
| end |
|
|
| sig { returns(Dependabot::Clients::GithubWithRetries) } |
| def github_client_for_source |
| @github_client_for_source ||= |
| T.let( |
| Dependabot::Clients::GithubWithRetries.for_source( |
| source: source, |
| credentials: credentials |
| ), |
| T.nilable(Dependabot::Clients::GithubWithRetries) |
| ) |
| end |
|
|
| sig { returns(T::Boolean) } |
| def pull_request_exists? |
| pull_request |
| true |
| rescue Octokit::NotFound |
| false |
| end |
|
|
| sig { returns(T.untyped) } |
| def pull_request |
| @pull_request ||= |
| T.let( |
| T.unsafe(github_client_for_source).pull_request( |
| source.repo, |
| pull_request_number |
| ), |
| T.untyped |
| ) |
| end |
|
|
| sig { params(name: String).returns(T::Boolean) } |
| def branch_exists?(name) |
| T.unsafe(github_client_for_source).branch(source.repo, name) |
| true |
| rescue Octokit::NotFound |
| false |
| end |
|
|
| sig { returns(T.untyped) } |
| def create_commit |
| tree = create_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 |
|
|
| begin |
| T.unsafe(github_client_for_source).create_commit( |
| source.repo, |
| commit_message, |
| tree.sha, |
| base_commit, |
| options |
| ) |
| rescue Octokit::UnprocessableEntity => e |
| raise unless e.message == "Tree SHA does not exist" |
|
|
| |
| |
| retry_count ||= 0 |
| retry_count += 1 |
| raise if retry_count > 10 |
|
|
| sleep(rand(1..1.99)) |
| retry |
| end |
| end |
|
|
| sig { returns(T.untyped) } |
| def create_tree |
| file_trees = files.map do |file| |
| if file.type == "submodule" |
| { |
| path: file.path.sub(%r{^/}, ""), |
| mode: Dependabot::DependencyFile::Mode::SUBMODULE, |
| type: "commit", |
| sha: file.content |
| } |
| else |
| content = if file.operation == Dependabot::DependencyFile::Operation::DELETE |
| { sha: nil } |
| elsif file.binary? |
| sha = T.unsafe(github_client_for_source).create_blob( |
| source.repo, file.content, "base64" |
| ) |
| { sha: sha } |
| else |
| { content: file.content } |
| end |
|
|
| { |
| path: file.realpath, |
| mode: Dependabot::DependencyFile::Mode::FILE, |
| type: "blob" |
| }.merge(content) |
| end |
| end |
|
|
| T.unsafe(github_client_for_source).create_tree( |
| source.repo, |
| file_trees, |
| base_tree: base_commit |
| ) |
| end |
|
|
| BRANCH_PROTECTION_ERROR_MESSAGES = T.let( |
| [ |
| /protected branch/i, |
| /not authorized to push/i, |
| /must not contain merge commits/i, |
| /required status check/i, |
| /cannot force-push to this branch/i, |
| /pull request for this branch has been added to a merge queue/i, |
| |
| /commits must have verified signatures/i, |
| /changes must be made through a pull request/i |
| ].freeze, |
| T::Array[Regexp] |
| ) |
|
|
| sig { params(commit: T.untyped).returns(T.untyped) } |
| def update_branch(commit) |
| T.unsafe(github_client_for_source).update_ref( |
| source.repo, |
| "heads/" + pull_request.head.ref, |
| commit.sha, |
| true |
| ) |
| rescue Octokit::UnprocessableEntity => e |
| |
| return nil if e.message.match?(/Reference does not exist/i) |
| return nil if e.message.match?(/Reference cannot be updated/i) |
|
|
| raise BranchProtected, e.message if BRANCH_PROTECTION_ERROR_MESSAGES.any? { |msg| e.message.match?(msg) } |
|
|
| raise |
| end |
|
|
| sig { returns(String) } |
| def commit_message |
| fallback_message = |
| "#{pull_request.title}" \ |
| "\n\n" \ |
| "Dependabot couldn't find the original pull request head commit, " \ |
| "#{old_commit}." |
|
|
| |
| |
| commit_being_updated&.message || fallback_message |
| end |
|
|
| sig { returns(T.untyped) } |
| def commit_being_updated |
| return @commit_being_updated if defined?(@commit_being_updated) |
|
|
| @commit_being_updated = |
| T.let( |
| if pull_request.commits == 1 |
| T.unsafe(github_client_for_source) |
| .git_commit(source.repo, pull_request.head.sha) |
| else |
| commits = |
| T.unsafe(github_client_for_source) |
| .pull_request_commits(source.repo, pull_request_number) |
|
|
| commit = commits.find { |c| c.sha == old_commit } |
| commit&.commit |
| end, |
| T.untyped |
| ) |
| end |
|
|
| sig { params(tree: T.untyped, author_details_with_date: T::Hash[Symbol, T.untyped]).returns(String) } |
| def commit_signature(tree, author_details_with_date) |
| PullRequestCreator::CommitSigner.new( |
| author_details: author_details_with_date, |
| commit_message: commit_message, |
| tree_sha: tree.sha, |
| parent_sha: base_commit, |
| signature_key: T.must(signature_key) |
| ).signature |
| end |
| end |
| end |
| end |
|
|