# typed: strong # frozen_string_literal: true require "sorbet-runtime" require "dependabot/metadata_finders" require "dependabot/credential" module Dependabot class PullRequestCreator # rubocop:disable Metrics/ClassLength extend T::Sig require "dependabot/pull_request_creator/azure" require "dependabot/pull_request_creator/bitbucket" require "dependabot/pull_request_creator/codecommit" require "dependabot/pull_request_creator/github" require "dependabot/pull_request_creator/gitlab" require "dependabot/pull_request_creator/message_builder" require "dependabot/pull_request_creator/branch_namer" require "dependabot/pull_request_creator/labeler" # Dependabot programmatically creates PRs which often include a large # number of links to objects on `github.com`. GitHub hydrates these into # rich links that leave a 'mention' on target Issues/Pull Requests. # # Due to the volume and nature of Dependabot PRs, these mentions are not # useful and can overwhelm maintainers, so we use a redirection service # to avoid enrichment. # # If you wish to disable this behaviour when using Dependabot Core directly, # pass a nil value when initialising this class. DEFAULT_GITHUB_REDIRECTION_SERVICE = "redirect.github.com" class RepoNotFound < StandardError; end class RepoArchived < StandardError; end class RepoDisabled < StandardError; end class NoHistoryInCommon < StandardError; end class UnmergedPRExists < StandardError; end class BranchAlreadyExists < StandardError; end class BaseCommitNotUpToDate < StandardError; end class UnexpectedError < StandardError; end # AnnotationError is raised if a PR was created, but failed annotation class AnnotationError < StandardError extend T::Sig sig { returns(StandardError) } attr_reader :cause # TODO: Currently, this error is only used by the GitHub PR creator. # An Octokit update will likely give this a proper type, # but we should consider a `Dependabot::PullRequest` type. sig { returns(Sawyer::Resource) } attr_reader :pull_request sig { params(cause: StandardError, pull_request: Sawyer::Resource).void } def initialize(cause, pull_request) super(cause.message) @cause = cause @pull_request = pull_request end end sig { returns(Dependabot::Source) } attr_reader :source sig { returns(T::Array[Dependabot::Dependency]) } attr_reader :dependencies sig { returns(T::Array[Dependabot::DependencyFile]) } attr_reader :files sig { returns(String) } attr_reader :base_commit sig { returns(T::Array[Dependabot::Credential]) } attr_reader :credentials sig { returns(T.nilable(String)) } attr_reader :pr_message_header sig { returns(T.nilable(String)) } attr_reader :pr_message_footer sig { returns(T.nilable(T::Array[String])) } attr_reader :custom_labels sig { returns(T.nilable(T::Hash[Symbol, String])) } attr_reader :author_details sig { returns(T.nilable(String)) } attr_reader :signature_key sig { returns(T::Hash[Symbol, T.untyped]) } attr_reader :commit_message_options sig { returns(T::Hash[String, String]) } attr_reader :vulnerabilities_fixed AzureReviewers = T.type_alias { T.nilable(T::Array[String]) } GithubReviewers = T.type_alias { T.nilable(T::Hash[String, T::Array[String]]) } GitLabReviewers = T.type_alias { T.nilable(T::Hash[Symbol, T::Array[Integer]]) } Reviewers = T.type_alias { T.any(AzureReviewers, GithubReviewers, GitLabReviewers) } sig { returns(Reviewers) } attr_reader :reviewers sig { returns(T.nilable(T.any(T::Array[String], T::Array[Integer]))) } attr_reader :assignees sig { returns(T.nilable(T.any(T::Array[String], Integer))) } attr_reader :milestone sig { returns(String) } attr_reader :branch_name_separator sig { returns(String) } attr_reader :branch_name_prefix sig { returns(T.nilable(Integer)) } attr_reader :branch_name_max_length sig { returns(String) } attr_reader :github_redirection_service sig { returns(T.nilable(T::Hash[String, String])) } attr_reader :custom_headers sig { returns(T.nilable(T::Hash[Symbol, T.untyped])) } attr_reader :provider_metadata sig { returns(T.nilable(Dependabot::DependencyGroup)) } attr_reader :dependency_group sig { returns(T.nilable(Integer)) } attr_reader :pr_message_max_length sig { returns(T.nilable(Encoding)) } attr_reader :pr_message_encoding sig do params( source: Dependabot::Source, base_commit: String, dependencies: T::Array[Dependabot::Dependency], files: T::Array[Dependabot::DependencyFile], credentials: T::Array[Dependabot::Credential], pr_message_header: T.nilable(String), pr_message_footer: T.nilable(String), custom_labels: T.nilable(T::Array[String]), author_details: T.nilable(T::Hash[Symbol, String]), signature_key: T.nilable(String), commit_message_options: T::Hash[Symbol, T.untyped], vulnerabilities_fixed: T::Hash[String, String], reviewers: Reviewers, assignees: T.nilable(T.any(T::Array[String], T::Array[Integer])), milestone: T.nilable(T.any(T::Array[String], Integer)), branch_name_separator: String, branch_name_prefix: String, branch_name_max_length: T.nilable(Integer), label_language: T::Boolean, automerge_candidate: T::Boolean, github_redirection_service: String, custom_headers: T.nilable(T::Hash[String, String]), require_up_to_date_base: T::Boolean, provider_metadata: T.nilable(T::Hash[Symbol, T.untyped]), message: T.nilable( T.any(Dependabot::PullRequestCreator::Message, Dependabot::PullRequestCreator::MessageBuilder) ), dependency_group: T.nilable(Dependabot::DependencyGroup), pr_message_max_length: T.nilable(Integer), pr_message_encoding: T.nilable(Encoding) ) .void end def initialize( source:, base_commit:, dependencies:, files:, credentials:, pr_message_header: nil, pr_message_footer: nil, custom_labels: nil, author_details: nil, signature_key: nil, commit_message_options: {}, vulnerabilities_fixed: {}, reviewers: nil, assignees: nil, milestone: nil, branch_name_separator: "/", branch_name_prefix: "dependabot", branch_name_max_length: nil, label_language: false, automerge_candidate: false, github_redirection_service: DEFAULT_GITHUB_REDIRECTION_SERVICE, custom_headers: nil, require_up_to_date_base: false, provider_metadata: {}, message: nil, dependency_group: nil, pr_message_max_length: nil, pr_message_encoding: nil ) @dependencies = dependencies @source = source @base_commit = base_commit @files = files @credentials = credentials @pr_message_header = pr_message_header @pr_message_footer = pr_message_footer @author_details = author_details @signature_key = signature_key @commit_message_options = commit_message_options @custom_labels = custom_labels @reviewers = reviewers @assignees = assignees @milestone = milestone @vulnerabilities_fixed = vulnerabilities_fixed @branch_name_separator = branch_name_separator @branch_name_prefix = branch_name_prefix @branch_name_max_length = branch_name_max_length @label_language = label_language @automerge_candidate = automerge_candidate @github_redirection_service = github_redirection_service @custom_headers = custom_headers @require_up_to_date_base = require_up_to_date_base @provider_metadata = provider_metadata @message = message @dependency_group = dependency_group @pr_message_max_length = pr_message_max_length @pr_message_encoding = pr_message_encoding check_dependencies_have_previous_version end sig { void } def check_dependencies_have_previous_version return if dependencies.all? { |d| requirements_changed?(d) } return if dependencies.all?(&:previous_version) raise "Dependencies must have a previous version or changed " \ "requirement to have a pull request created for them!" end # TODO: This returns client-specific objects. # We should create a standard interface (`Dependabot::PullRequest`) and # then convert to that sig { returns(T.untyped) } def create case source.provider when "github" then github_creator.create when "gitlab" then gitlab_creator.create when "azure" then azure_creator.create when "bitbucket" then bitbucket_creator.create when "codecommit" then codecommit_creator.create else raise "Unsupported provider #{source.provider}" end end private sig { returns(T::Boolean) } def label_language? @label_language end sig { returns(T::Boolean) } def automerge_candidate? @automerge_candidate end sig { returns(T::Boolean) } def require_up_to_date_base? @require_up_to_date_base end sig { returns(Dependabot::PullRequestCreator::Github) } def github_creator Github.new( source: source, branch_name: branch_namer.new_branch_name, base_commit: base_commit, credentials: credentials, files: files, commit_message: T.must(message.commit_message), pr_description: T.must(message.pr_message), pr_name: T.must(message.pr_name), author_details: author_details, signature_key: signature_key, labeler: labeler, reviewers: T.cast(reviewers, GithubReviewers), assignees: T.cast(assignees, T.nilable(T::Array[String])), milestone: T.cast(milestone, T.nilable(Integer)), custom_headers: custom_headers, require_up_to_date_base: require_up_to_date_base? ) end sig { returns(Dependabot::PullRequestCreator::Gitlab) } def gitlab_creator Gitlab.new( source: source, branch_name: branch_namer.new_branch_name, base_commit: base_commit, credentials: credentials, files: files, commit_message: T.must(message.commit_message), pr_description: T.must(message.pr_message), pr_name: T.must(message.pr_name), author_details: author_details, labeler: labeler, approvers: T.cast(reviewers, T.nilable(T::Hash[Symbol, T::Array[Integer]])), assignees: T.cast(assignees, T.nilable(T::Array[Integer])), milestone: milestone, target_project_id: T.cast(provider_metadata&.fetch(:target_project_id, nil), T.nilable(Integer)) ) end sig { returns(Dependabot::PullRequestCreator::Azure) } def azure_creator Azure.new( source: source, branch_name: branch_namer.new_branch_name, base_commit: base_commit, credentials: credentials, files: files, commit_message: T.must(message.commit_message), pr_description: T.must(message.pr_message), pr_name: T.must(message.pr_name), author_details: author_details, labeler: labeler, reviewers: T.cast(reviewers, AzureReviewers), assignees: T.cast(assignees, T.nilable(T::Array[String])), work_item: T.cast(provider_metadata&.fetch(:work_item, nil), T.nilable(Integer)) ) end sig { returns(Dependabot::PullRequestCreator::Bitbucket) } def bitbucket_creator Bitbucket.new( source: source, branch_name: branch_namer.new_branch_name, base_commit: base_commit, credentials: credentials, files: files, commit_message: T.must(message.commit_message), pr_description: T.must(message.pr_message), pr_name: T.must(message.pr_name), author_details: author_details, labeler: nil, work_item: T.cast(provider_metadata&.fetch(:work_item, nil), T.nilable(Integer)) ) end sig { returns(Dependabot::PullRequestCreator::Codecommit) } def codecommit_creator Codecommit.new( source: source, branch_name: branch_namer.new_branch_name, base_commit: base_commit, credentials: credentials, files: files, commit_message: T.must(message.commit_message), pr_description: T.must(message.pr_message), pr_name: T.must(message.pr_name), author_details: author_details, labeler: labeler, require_up_to_date_base: require_up_to_date_base? ) end sig { returns(T.any(Dependabot::PullRequestCreator::Message, Dependabot::PullRequestCreator::MessageBuilder)) } def message return @message unless @message.nil? case source.provider when "github" @pr_message_max_length = Github::PR_DESCRIPTION_MAX_LENGTH if @pr_message_max_length.nil? when "azure" @pr_message_max_length = Azure::PR_DESCRIPTION_MAX_LENGTH if @pr_message_max_length.nil? @pr_message_encoding = Azure::PR_DESCRIPTION_ENCODING if @pr_message_encoding.nil? when "codecommit" @pr_message_max_length = Codecommit::PR_DESCRIPTION_MAX_LENGTH if @pr_message_max_length.nil? when "bitbucket" @pr_message_max_length = Bitbucket::PR_DESCRIPTION_MAX_LENGTH if @pr_message_max_length.nil? end @message = MessageBuilder.new( source: source, dependencies: dependencies, files: files, credentials: credentials, commit_message_options: commit_message_options, pr_message_header: pr_message_header, pr_message_footer: pr_message_footer, vulnerabilities_fixed: vulnerabilities_fixed, github_redirection_service: github_redirection_service, dependency_group: dependency_group, pr_message_max_length: pr_message_max_length, pr_message_encoding: pr_message_encoding ) end sig { returns(Dependabot::PullRequestCreator::BranchNamer) } def branch_namer @branch_namer ||= T.let( BranchNamer.new( dependencies: dependencies, files: files, target_branch: source.branch, dependency_group: dependency_group, separator: branch_name_separator, prefix: branch_name_prefix, max_length: branch_name_max_length, includes_security_fixes: includes_security_fixes? ), T.nilable(Dependabot::PullRequestCreator::BranchNamer) ) end sig { returns(Dependabot::PullRequestCreator::Labeler) } def labeler @labeler ||= T.let( Labeler.new( source: source, custom_labels: custom_labels, credentials: credentials, includes_security_fixes: includes_security_fixes?, dependencies: dependencies, label_language: label_language?, automerge_candidate: automerge_candidate? ), T.nilable(Dependabot::PullRequestCreator::Labeler) ) end sig { returns(T::Boolean) } def includes_security_fixes? vulnerabilities_fixed.values.flatten.any? end sig { params(dependency: Dependabot::Dependency).returns(T::Boolean) } def requirements_changed?(dependency) (dependency.requirements - T.must(dependency.previous_requirements)).any? end end end