mirror of
https://github.com/gitlabhq/gitlabhq.git
synced 2025-07-25 17:08:32 +00:00
202 lines
6.4 KiB
Ruby
Executable File
202 lines
6.4 KiB
Ruby
Executable File
#!/usr/bin/env ruby
|
|
# frozen_string_literal: true
|
|
|
|
if Object.const_defined?(:RSpec)
|
|
# Ok, we're testing, we know we're going to stub `Gitlab`, so we just ignore
|
|
else
|
|
require 'gitlab'
|
|
|
|
if Gitlab.singleton_class.method_defined?(:com?)
|
|
abort 'lib/gitlab.rb is loaded, and this means we can no longer load the client and we cannot proceed'
|
|
end
|
|
end
|
|
|
|
require 'time'
|
|
|
|
class PreMergeChecks
|
|
DEFAULT_API_ENDPOINT = "https://gitlab.com/api/v4"
|
|
MERGE_TRAIN_REF_REGEX = %r{\Arefs/merge-requests/\d+/train\z}
|
|
PIPELINE_FRESHNESS_DEFAULT_THRESHOLD_IN_HOURS = 8
|
|
PIPELINE_FRESHNESS_STABLE_BRANCHES_THRESHOLD_IN_HOURS = 72
|
|
PREDICTIVE_PIPELINE_IDENTIFIER = 'predictive'
|
|
REQUIRED_TIER_IDENTIFIER = 'tier:3'
|
|
STABLE_BRANCH_SUFFIX = '-stable-ee'
|
|
TIER_IDENTIFIER_REGEX = /tier:\d/
|
|
|
|
PreMergeChecksFailedError = Class.new(StandardError)
|
|
PreMergeChecksStatus = Struct.new(:exitstatus, :message) do
|
|
def success?
|
|
exitstatus == 0
|
|
end
|
|
end
|
|
|
|
def initialize(
|
|
api_endpoint: ENV.fetch('CI_API_V4_URL', DEFAULT_API_ENDPOINT),
|
|
project_id: ENV['CI_PROJECT_ID'],
|
|
merge_request_iid: ENV['CI_MERGE_REQUEST_IID'],
|
|
target_branch: ENV['CI_MERGE_REQUEST_TARGET_BRANCH_NAME'])
|
|
@api_endpoint = api_endpoint
|
|
@project_id = project_id
|
|
@merge_request_iid = merge_request_iid.to_i
|
|
@target_branch = target_branch
|
|
end
|
|
|
|
def execute
|
|
check_required_ids!
|
|
|
|
# Find the first non merge-train pipeline in project with id `project_id`
|
|
latest_pipeline_id = api_client.merge_request_pipelines(project_id, merge_request_iid).auto_paginate do |pipeline|
|
|
next if pipeline.ref.match?(MERGE_TRAIN_REF_REGEX)
|
|
# We ensure we are comparing strings (not integers and strings)
|
|
next if pipeline.project_id.to_s != project_id.to_s
|
|
|
|
break pipeline.id
|
|
end
|
|
|
|
unless latest_pipeline_id
|
|
fail_check!("Expected to have a latest pipeline that ran in project ##{project_id} but got none!")
|
|
end
|
|
|
|
latest_pipeline = api_client.pipeline(project_id, latest_pipeline_id)
|
|
|
|
check_pipeline_for_merged_results!(latest_pipeline)
|
|
check_pipeline_success!(latest_pipeline)
|
|
check_pipeline_identifier!(latest_pipeline)
|
|
check_pipeline_freshness!(latest_pipeline)
|
|
|
|
PreMergeChecksStatus.new(0, "All good for merge! 🚀")
|
|
rescue PreMergeChecksFailedError => ex
|
|
PreMergeChecksStatus.new(1, ex.message)
|
|
end
|
|
|
|
private
|
|
|
|
attr_reader :api_endpoint, :project_id, :merge_request_iid, :target_branch
|
|
|
|
def api_client
|
|
@api_client ||= begin
|
|
Gitlab.configure do |config|
|
|
config.endpoint = api_endpoint
|
|
config.private_token = ENV.fetch('GITLAB_API_PRIVATE_TOKEN', '')
|
|
end
|
|
Gitlab.client
|
|
end
|
|
end
|
|
|
|
def check_required_ids!
|
|
fail_check!("Missing project_id!") unless project_id
|
|
fail_check!("Missing merge_request_iid!") if merge_request_iid == 0
|
|
fail_check!("Missing target_branch!") unless target_branch
|
|
end
|
|
|
|
def check_pipeline_for_merged_results!(pipeline)
|
|
return if pipeline.ref == "refs/merge-requests/#{merge_request_iid}/merge"
|
|
|
|
fail_check! <<~TEXT
|
|
Expected to have a Merged Results pipeline but got #{pipeline.ref}!
|
|
|
|
Please start a new pipeline.
|
|
TEXT
|
|
end
|
|
|
|
def check_pipeline_success!(pipeline)
|
|
return if pipeline.status == 'success'
|
|
|
|
fail_check! <<~TEXT
|
|
Expected latest pipeline (#{pipeline.web_url}) to be successful! Pipeline status was "#{pipeline.status}".
|
|
|
|
Please start a new pipeline.
|
|
TEXT
|
|
end
|
|
|
|
def check_pipeline_freshness!(pipeline)
|
|
hours_ago = ((Time.now - Time.parse(pipeline.created_at)) / 3600).ceil(2)
|
|
threshold =
|
|
if target_branch_is_stable_branch?
|
|
PIPELINE_FRESHNESS_STABLE_BRANCHES_THRESHOLD_IN_HOURS
|
|
else
|
|
PIPELINE_FRESHNESS_DEFAULT_THRESHOLD_IN_HOURS
|
|
end
|
|
|
|
return if hours_ago < threshold
|
|
|
|
fail_check! <<~TEXT
|
|
Expected latest pipeline (#{pipeline.web_url}) to be created within the last #{threshold} hours (it was created #{hours_ago} hours ago)!
|
|
|
|
Please start a new pipeline.
|
|
TEXT
|
|
end
|
|
|
|
def check_pipeline_identifier!(pipeline)
|
|
if pipeline.name.match?(TIER_IDENTIFIER_REGEX) && !pipeline.name.include?(REQUIRED_TIER_IDENTIFIER) # rubocop:disable Rails/NegateInclude -- Not executed in Rails context
|
|
fail_check! <<~MSG
|
|
Expected latest pipeline (#{pipeline.web_url}) to be a tier-3 pipeline! Pipeline name was "#{pipeline.name}".
|
|
|
|
Please ensure the MR has all the required approvals, start a new pipeline and put the MR back on the Merge Train.
|
|
MSG
|
|
end
|
|
|
|
return unless pipeline.name.include?(PREDICTIVE_PIPELINE_IDENTIFIER)
|
|
|
|
fail_check! <<~MSG
|
|
Expected latest pipeline (#{pipeline.web_url}) not to be a predictive pipeline! Pipeline name was "#{pipeline.name}".
|
|
|
|
Please ensure the MR has all the required approvals, start a new pipeline and put the MR back on the Merge Train.
|
|
MSG
|
|
end
|
|
|
|
def target_branch_is_stable_branch?
|
|
target_branch.end_with?(STABLE_BRANCH_SUFFIX)
|
|
end
|
|
|
|
def fail_check!(text)
|
|
raise PreMergeChecksFailedError, text
|
|
end
|
|
end
|
|
|
|
if $PROGRAM_NAME == __FILE__
|
|
require 'optparse'
|
|
options = {}
|
|
|
|
OptionParser.new do |opts|
|
|
opts.on("-p", "--project_id [string]", String, "Project ID") do |value|
|
|
options[:project_id] = value
|
|
end
|
|
|
|
opts.on("-m", "--merge_request_iid [string]", String, "Merge request IID") do |value|
|
|
options[:merge_request_iid] = value
|
|
end
|
|
|
|
opts.on("-t", "--target-branch [string]", String, "Target branch name") do |value|
|
|
options[:target_branch] = value
|
|
end
|
|
|
|
opts.on("-h", "--help") do
|
|
puts "Usage: #{File.basename(__FILE__)} [--project_id <PROJECT_ID>] " \
|
|
"[--merge_request_iid <MERGE_REQUEST_IID>] [--target-branch <TARGET_BRANCH>]"
|
|
puts
|
|
puts "Examples:"
|
|
puts
|
|
puts "#{File.basename(__FILE__)} --project_id \"gitlab-org/gitlab\" " \
|
|
"--merge_request_iid \"1\" --target-branch \"master\""
|
|
|
|
exit
|
|
end
|
|
end.parse!
|
|
|
|
colors_to_codes = {
|
|
red: 31,
|
|
green: 32
|
|
}.freeze
|
|
|
|
pre_merge_checks_status = PreMergeChecks.new(**options).execute
|
|
|
|
if pre_merge_checks_status.success?
|
|
puts "\e[#{colors_to_codes[:green]}m#{pre_merge_checks_status.message}\e[0m"
|
|
else
|
|
puts "\e[#{colors_to_codes[:red]}m#{pre_merge_checks_status.message}\e[0m"
|
|
end
|
|
|
|
exit(pre_merge_checks_status.exitstatus)
|
|
end
|