lib/release_tools/cherry_pick/auto_deploy_service.rb (189 lines of code) (raw):

# frozen_string_literal: true module ReleaseTools module CherryPick # Performs automated cherry-picking into an auto-deploy branch # # Given a `project`, gathers merged MRs labeled `Pick into auto-deploy` and # attempts to cherry-pick their merge commit into the auto-deploy branch # specified by `target`. # # Each MR will receive a comment indicating the result of the attempt. class AutoDeployService # Raised when a target merge commit isn't available on Security MirrorError = Class.new(StandardError) include ::SemanticLogger::Loggable attr_reader :project attr_reader :target # project - ReleaseTools::Project object # target - Auto-deploy branch name def initialize(project, target) @project = project @target = target @results = [] end def execute return [] if no_pickable_mrs? canonical_pickable_mrs.auto_paginate do |merge_request| handle_merge_request(merge_request) end return @results unless Feature.enabled?(:cherry_pick_security_merge_requests) security_pickable_mrs.auto_paginate do |merge_request| handle_merge_request(merge_request) end @results end private def client ReleaseTools::GitlabClient end def notifier @notifier ||= AutoDeployNotifier.new(target, project) end def no_pickable_mrs? return canonical_pickable_mrs.none? unless Feature.enabled?(:cherry_pick_security_merge_requests) canonical_pickable_mrs.none? && security_pickable_mrs.none? end def handle_merge_request(merge_request) result = if allowed?(merge_request) assert_mirrored!(merge_request) if target_includes_merge_commit?(merge_request) Result.new( merge_request: merge_request, status: :completed, reason: "Auto deploy branch #{target} already contains MR merge commit" ) elsif target_includes_cherry_picked_commit?(merge_request) Result.new( merge_request: merge_request, status: :not_required, reason: "Auto deploy branch #{target} already contains a cherry-picked commit" ) else cherry_pick(merge_request) end else Result.new(merge_request: merge_request, status: :denied, reason: 'Missing ~severity::1 or ~severity::2 label') end record_result(result) end def canonical_pickable_mrs @canonical_pickable_mrs ||= Retriable.with_context(:api) do client.merge_requests( project, state: 'merged', labels: PickIntoLabel.for(:auto_deploy), order_by: 'created_at', sort: 'asc' ) end end def security_pickable_mrs @security_pickable_mrs ||= Retriable.with_context(:api) do client.merge_requests( project.security_path, state: 'merged', labels: PickIntoLabel.for(:auto_deploy), order_by: 'created_at', sort: 'asc' ) end end # Attempt to cherry-pick a merge request into the auto-deploy branch # # Returns a Result object def cherry_pick(merge_request) cherry_pick_result = unless SharedStatus.dry_run? client.cherry_pick( project.auto_deploy_path, ref: merge_request.merge_commit_sha, target: @target ) end Result.new(merge_request: merge_request, status: :success, new_sha: cherry_pick_result&.short_id) rescue Gitlab::Error::Error => ex Result.new(merge_request: merge_request, status: :failure, reason: ex.message) end def allowed?(merge_request) merge_request.labels.include?('severity::1') || merge_request.labels.include?('severity::2') end def target_includes_merge_commit?(merge_request) client.commit_refs(project.auto_deploy_path, merge_request.merge_commit_sha, type: 'branch').each_page do |refs| result = refs.detect { |ref| ref.name == target } logger.info( 'Searching for merge commit in auto deploy branch', project: project.auto_deploy_path, sha: merge_request.merge_commit_sha, branch: target, present_in_branch: result ) return true if result end false end def target_includes_cherry_picked_commit?(merge_request) sha = merge_request.merge_commit_sha results = client.search_in_project( project.auto_deploy_path, 'commits', "cherry picked from commit #{sha}", target ) .flatten .uniq(&:id) logger.info( 'Searching for cherry-picked commits in auto deploy branch', project: project.auto_deploy_path, merge_commit: merge_request.merge_commit_sha, cherry_picked_commit: results.map(&:web_url), branch: target ) results.present? end # Verify the merge commit has been mirrored to the Security repository def assert_mirrored!(merge_request) # Skip security mirror check for security MRs return if merge_request.web_url.include?('gitlab-org/security') Retriable.with_context(:api, tries: 10) do client.commit(project.security_path, ref: merge_request.merge_commit_sha) end rescue ::Gitlab::Error::NotFound # If the commit still isn't available after retries, something may be # wrong with our mirroring or merge-train, and we should fail loudly message = <<~MSG Unable to find #{merge_request.merge_commit_sha} on #{project.security_path}. Check that canonical is successfully mirroring to security. If it is not (for example during a patch release), manually trigger the `gitlab-org/gitlab@master -> gitlab-org/security/gitlab@master` merge-train pipeline schedule: https://ops.gitlab.net/gitlab-org/merge-train/-/pipeline_schedules MSG raise MirrorError, message end def record_result(result) @results << result track_result(result) log_result(result) notifier.comment(result) end def log_result(result) payload = { project: project, target: @target, merge_request: result.url } payload[:reason] = result.reason if result.reason if result.success? logger.info('Cherry-pick merged', payload) elsif result.denied? logger.info('Cherry-pick denied', payload) elsif result.failure? logger.warn('Cherry-pick failed', payload) elsif result.not_required? logger.info('Cherry-pick not required', payload) elsif result.completed? logger.info('Merge commit included', payload) else logger.warn('Unknown status for cherry-pick', payload) end end def track_result(result) label = if result.success? 'success' elsif result.failure? || result.denied? 'failed' else # Don't count not_required and completed statuses since no action was taken return end Metrics::Client.new.inc('auto_deploy_picks_total', labels: label) rescue StandardError => ex logger.error('pushing auto_deploy_picks_total metrics failed', error: ex) end end end end