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