| | |
| | |
| |
|
| | 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 |
| |
|