Add support for merging security merge requests

This adds the ability to automatically merge security merge requests
(should they be valid) using chatops. Once the merging is complete, a
Slack notification is sent informing the user about how many merge
requests were merged and which ones could not be merged.
parent 3714da20
......@@ -153,7 +153,7 @@ validate-security-merge-requests:
<<: *with-bundle
stage: automation
script:
- bundle exec rake validate_security_merge_requests
- bundle exec rake security:validate
only:
refs:
- schedules
......
......@@ -316,7 +316,24 @@ task :freeze do
HTTParty.post(webhook_url, body: { payload: JSON.dump(text: message) })
end
desc 'Validate security merge requests'
task :validate_security_merge_requests do
ReleaseTools::Security::MergeRequestsValidator.new.execute
namespace :security do
desc 'Validate security merge requests'
task :validate do
ReleaseTools::Security::MergeRequestsValidator.new.execute
end
desc 'Merges valid security merge requests'
task :merge, [:merge_master] do |_, args|
merge_master =
if args[:merge_master] && !args[:merge_master].empty?
true
else
false
end
ReleaseTools::Security::MergeRequestsMerger
.new(merge_master: merge_master)
.execute
end
end
......@@ -53,6 +53,9 @@ case $1 in
"tag_security")
bundle exec rake "release_managers:auth[$RELEASE_USER]" "$1[$RELEASE_VERSION]"
;;
"security:merge")
bundle exec rake "release_managers:auth[$RELEASE_USER]" "$1[$MERGE_MASTER_SECURITY_MERGE_REQUESTS]"
;;
*)
echo "Don't know what to do with $1!"
exit 1
......
......@@ -117,6 +117,8 @@ require 'release_tools/security/client'
require 'release_tools/security/pipeline'
require 'release_tools/security/merge_requests_validator'
require 'release_tools/security/merge_request_validator'
require 'release_tools/security/merge_requests_merger'
require 'release_tools/security/merge_result'
unless ENV['TEST']
require 'sentry-raven'
......
# frozen_string_literal: true
module ReleaseTools
module Security
# Merging of valid security merge requests across different projects.
class MergeRequestsMerger
ERROR_TEMPLATE = <<~ERROR.strip
@%<author_username>s
This merge request could not be merged automatically. Please rebase this
merge request with the target branch and resolve any conflicts that may
appear. Once resolved and the pipelines have passed, assign this merge
request back to me and mark this discussion as resolved.
<hr>
<sub>
:robot: This is an automated message generated using the
[release tools project](https://gitlab.com/gitlab-org/release-tools/).
If you believe there is an error, please create an issue in the
release tools project.
</sub>
ERROR
# @param [TrueClass|FalseClass] merge_master If merge requests that target
# `master` should also be merged.
def initialize(merge_master: false)
@merge_master = merge_master
@client = Client.new
end
# Merges all valid security merge requests.
def execute
valid = validated_merge_requests
tuples = Parallel.map(valid, in_threads: Etc.nprocessors) do |mr|
[merge(mr), mr]
end
merge_result = MergeResult.from_array(tuples)
Slack::ChatopsNotification.merged_security_merge_requests(merge_result)
end
def validated_merge_requests
valid = MergeRequestsValidator.new.execute
if @merge_master
valid
else
valid.reject { |mr| mr.target_branch == 'master' }
end
end
# @param [Gitlab::ObjectifiedHash] mr
def merge(mr)
merged_mr = @client.accept_merge_request(mr.project_id, mr.iid)
if merged_mr.respond_to?(:merge_commit_sha) && merged_mr.merge_commit_sha
true
else
@client.create_merge_request_discussion(
mr.project_id,
mr.iid,
body: format(ERROR_TEMPLATE, author_username: mr.author.username)
)
@client.update_merge_request(
mr.project_id,
mr.iid,
assignee_id: mr.author.id
)
false
end
end
end
end
end
# frozen_string_literal: true
module ReleaseTools
module Security
class MergeResult
def self.from_array(array)
merged = []
not_merged = []
array.each do |(did_merge, mr)|
if did_merge
merged << mr
else
not_merged << mr
end
end
new(merged: merged, not_merged: not_merged)
end
def initialize(merged: [], not_merged: [])
@merged = merged
@not_merged = not_merged
end
def merged_count
@merged.length
end
def not_merged_count
@not_merged.length
end
def not_merged_slack_attachment_fields
@not_merged.group_by(&:target_branch).map do |(target_branch, mrs)|
{
title: "Branch: #{target_branch}",
value: mrs.map { |mr| "<#{mr.web_url}|!#{mr.iid}>" }.join(', '),
short: false
}
end
end
def slack_attachments
attachments = []
if @merged.any?
attachments << {
fallback: "Merged: #{merged_count}",
title: ":heavy_check_mark: Merged: #{merged_count}",
color: 'good'
}
end
if @not_merged.any?
attachments << {
fallback: "Not merged: #{not_merged_count}",
title: ":x: Not merged: #{not_merged_count}",
color: 'danger',
fields: not_merged_slack_attachment_fields
}
end
attachments
end
end
end
end
......@@ -33,6 +33,15 @@ module ReleaseTools
fire_hook(text: text, attachments: [attachment], channel: channel)
end
# @param [ReleaseTools::Security::MergeResult] merge_result
def self.merged_security_merge_requests(result)
fire_hook(
text: 'Finished merging security merge requests',
channel: channel,
attachments: result.slack_attachments
)
end
end
end
end
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment