lib/release_tools/security/implementation_issue.rb (197 lines of code) (raw):

# frozen_string_literal: true module ReleaseTools module Security class ImplementationIssue include ::SemanticLogger::Loggable # Number of merge requests that has to be associated to every Security Issue MERGE_REQUESTS_SIZE = 4 # Label indicating issue doesn't require full backports BACKPORT_EXCEPTION_LABEL = 'reduced backports' # Internal ID of GitLab Release Bot GITLAB_RELEASE_BOT_ID = 2_324_599 # Format of stable branches on GitLab repos STABLE_BRANCH_REGEX = /^(\d+-\d+-stable(-ee)?)$/ # Collection of security projects that are allowed to early merge PROJECTS_ALLOWED_TO_EARLY_MERGE = [ Project::GitlabEe.security_id, Project::OmnibusGitlab.security_id, Project::CNGImage.security_id ].freeze DEFAULT_BRANCHES = %w[main master].freeze # rubocop:disable ReleaseTools/DefaultBranchLiteral # The status of merge requests to be considered OPENED = 'opened' MERGED = 'merged' ALLOWED_DEFAULT_MR_STATE = [OPENED, MERGED].freeze attr_reader :project_id, :iid, :web_url, :reference, :merge_requests, :issue delegate :labels, to: :@issue delegate :assignees, to: :@issue delegate :author, to: :@issue delegate :title, to: :@issue # this allows usage of GitLabClient methods that use `project_path` alias project project_id def initialize(issue, merge_requests) @issue = issue @project_id = issue.project_id @iid = issue.iid @web_url = issue.web_url @reference = issue.references.full @merge_requests = merge_requests end def ready_to_be_processed? validate pending_reasons.empty? end def pending_reasons return [] unless @reasons @reasons end def merge_requests_targeting_default_branch @merge_requests_targeting_default_branch ||= merge_requests .select { |merge_request| DEFAULT_BRANCHES.include?(merge_request.target_branch) } end # Only returns the first MR targeting the default branch def merge_request_targeting_default_branch @merge_request_targeting_default_branch ||= merge_requests_targeting_default_branch.first end def default_merge_request_handled? return merge_request_targeting_default_branch.state == MERGED if merge_request_targeting_default_branch.present? @issue.labels.include?(BACKPORT_EXCEPTION_LABEL) end def mwps_set_on_default_merge_request? merge_request_targeting_default_branch.present? && merge_request_targeting_default_branch.state == OPENED && merge_request_targeting_default_branch.merge_when_pipeline_succeeds end def default_merge_request_deployed? return false unless merge_request_targeting_default_branch # Check if the API returns the default branch MR when filtering by environment mrs = GitlabClient.merge_requests( merge_request_targeting_default_branch.project_id, { iids: [merge_request_targeting_default_branch.iid], environment: 'gprd' } ) # Check if the first_deployed_to_production_at attribute is a reliable way to check if an MR has been deployed # to gprd. logger.info( 'Security default branch MR deployed to gprd?', environment_filter: !mrs.empty?, first_deployed_to_production_at: merge_request_targeting_default_branch.first_deployed_to_production_at.present?, deployment_timestamp: merge_request_targeting_default_branch.first_deployed_to_production_at, mr_url: merge_request_targeting_default_branch.web_url ) !mrs.empty? end def backports @backports ||= merge_requests .select { |merge_request| merge_request.target_branch.match?(STABLE_BRANCH_REGEX) } end def backports_merged? !backports.empty? && backports.all? { |mr| mr.state == MERGED } end def processed? merge_requests.all? { |mr| mr.state == MERGED } end def allowed_to_early_merge? merge_request_targeting_default_branch&.state == OPENED && PROJECTS_ALLOWED_TO_EARLY_MERGE.include?(merge_request_targeting_default_branch.project_id) end def pending_merge_requests if PROJECTS_ALLOWED_TO_EARLY_MERGE.include?(project_id) backports else opened_merge_requests end end def security_target? @issue.labels.include?(ReleaseTools::Security::IssueCrawler::SECURITY_TARGET_LABEL) end def cves_issue issue = RelatedIssuesFinder.new(self).cves_issue return unless issue CvesIssue.new(issue) end def canonical_issue RelatedIssuesFinder.new(self).canonical_issue end def opened_merge_requests_targeting_default_branch merge_requests_targeting_default_branch.select { |merge_request| merge_request.state == OPENED } end private def validate return if defined?(@reasons) @reasons = [] validate_merge_requests_present validate_missing_backports validate_default_mr_status validate_backport_mr_status validate_merge_requests_assigned_to_the_bot validate_merge_requests end def reject(reason) logger.warn("Rejecting implementation issue due to #{reason}", url: web_url) @reasons << reason end def validate_merge_requests_present return unless merge_requests.empty? reject( <<~EOF.squish There are no MRs linked to this issue. Follow the steps in the issue description to create the required MRs. EOF ) end def validate_missing_backports return unless merge_requests.length < MERGE_REQUESTS_SIZE && @issue.labels.exclude?(BACKPORT_EXCEPTION_LABEL) reject( <<~EOF.squish Every security issue is expected to have 4 MRs; 3 backports targeting the versions in the [next scheduled patch release](https://gitlab.com/gitlab-org/gitlab/-/issues/?label_name%5B%5D=upcoming%20security%20release) and 1 MR targeting master. If this issue legitimately does not need to have all 4 MRs, please add the ~"reduced backports" label to this issue EOF ) end def validate_merge_requests_assigned_to_the_bot unassigned_mrs = merge_requests.reject do |merge_request| merge_request .assignees .map { |assignee| assignee.to_h.transform_keys(&:to_sym)[:id] }.include?(GITLAB_RELEASE_BOT_ID) end return if unassigned_mrs.empty? reject( <<~EOF.squish The following merge requests need to be assigned to the `@gitlab-release-tools-bot`: #{unassigned_mrs.map(&:web_url).join(', ')} EOF ) end def validate_default_mr_status return unless merge_request_targeting_default_branch default_branch_state = merge_request_targeting_default_branch.state return if ALLOWED_DEFAULT_MR_STATE.include?(default_branch_state) reject( <<~EOF.squish The [MR targeting the default branch](#{merge_request_targeting_default_branch.web_url}) needs to be in one of the following states: #{ALLOWED_DEFAULT_MR_STATE.join(', ')} EOF ) end def validate_backport_mr_status return if backports.all? { |mr| mr.state == OPENED } reject( <<~EOF.squish The following backport MRs need to be in the 'opened' state: #{backports.reject { |mr| mr.state == OPENED }.map(&:web_url).join(', ')} EOF ) end def validate_merge_requests invalid_mrs = ReleaseTools::Security::MergeRequestsValidator .new(ReleaseTools::Security::Client.new) .execute(merge_requests: opened_merge_requests) .last return if invalid_mrs.empty? reject( <<~EOF.squish The following merge requests are not ready. A comment has been posted in each MR with more details: #{invalid_mrs.map(&:web_url).join(', ')} EOF ) end def opened_merge_requests merge_requests .select { |merge_request| merge_request.state == OPENED } end end end end