Skip to content
代码片段 群组 项目
未验证 提交 f614c777 编辑于 作者: Rémy Coutable's avatar Rémy Coutable
浏览文件

ci: Add a new pre-merge-checks job


Signed-off-by: default avatarRémy Coutable <remy@rymai.me>
上级 3c02c53c
No related branches found
No related tags found
无相关合并请求
...@@ -10,6 +10,7 @@ stages: ...@@ -10,6 +10,7 @@ stages:
- review - review
- qa - qa
- post-qa - post-qa
- pre-merge
- pages - pages
- notify - notify
- release-environments - release-environments
......
pre-merge-checks:
extends:
- .pre-merge:rules:pre-merge-checks
- .fast-no-clone-job
variables:
# We use > instead of | because we want the files to be space-separated.
FILES_TO_DOWNLOAD: >
scripts/utils.sh
scripts/pipeline/merge-train-checks.rb
image: ${GITLAB_DEPENDENCY_PROXY_ADDRESS}ruby:${RUBY_VERSION}-alpine3.16
stage: pre-merge
script:
- install_gitlab_gem
- chmod u+x scripts/pipeline/merge-train-checks.rb && scripts/pipeline/merge-train-checks.rb
...@@ -152,6 +152,9 @@ ...@@ -152,6 +152,9 @@
.if-dot-com-gitlab-org-merge-request: &if-dot-com-gitlab-org-merge-request .if-dot-com-gitlab-org-merge-request: &if-dot-com-gitlab-org-merge-request
if: '$CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE == "gitlab-org" && ($CI_MERGE_REQUEST_EVENT_TYPE == "merged_result" || $CI_MERGE_REQUEST_EVENT_TYPE == "detached")' if: '$CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE == "gitlab-org" && ($CI_MERGE_REQUEST_EVENT_TYPE == "merged_result" || $CI_MERGE_REQUEST_EVENT_TYPE == "detached")'
.if-dot-com-gitlab-org-and-subgroups-merge-train: &if-dot-com-gitlab-org-and-subgroups-merge-train
if: '$CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_NAMESPACE =~ /^gitlab-org/ && $CI_MERGE_REQUEST_EVENT_TYPE == "merge_train"'
.if-dot-com-gitlab-org-ee-tag: &if-dot-com-gitlab-org-ee-tag .if-dot-com-gitlab-org-ee-tag: &if-dot-com-gitlab-org-ee-tag
if: '$CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_PATH == "gitlab-org/gitlab" && $CI_COMMIT_TAG =~ /^v?[\d]+\.[\d]+\.[\d]+[\d\w-]*-ee$/' if: '$CI_SERVER_HOST == "gitlab.com" && $CI_PROJECT_PATH == "gitlab-org/gitlab" && $CI_COMMIT_TAG =~ /^v?[\d]+\.[\d]+\.[\d]+[\d\w-]*-ee$/'
...@@ -3321,3 +3324,10 @@ ...@@ -3321,3 +3324,10 @@
changes: *ci-patterns changes: *ci-patterns
when: manual when: manual
allow_failure: true allow_failure: true
##########################
# Pre-merge checks rules #
##########################
.pre-merge:rules:pre-merge-checks:
rules:
- <<: *if-dot-com-gitlab-org-and-subgroups-merge-train
...@@ -73,7 +73,6 @@ set-pipeline-name: ...@@ -73,7 +73,6 @@ set-pipeline-name:
- apk add --no-cache --update curl # Not present in ruby-alpine, so we add it manually - apk add --no-cache --update curl # Not present in ruby-alpine, so we add it manually
- !reference [".fast-no-clone-job", before_script] - !reference [".fast-no-clone-job", before_script]
script: script:
- source scripts/utils.sh
- install_gitlab_gem - install_gitlab_gem
- chmod u+x scripts/pipeline/set-pipeline-name && scripts/pipeline/set-pipeline-name - chmod u+x scripts/pipeline/set-pipeline-name && scripts/pipeline/set-pipeline-name
allow_failure: allow_failure:
......
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'time'
require 'gitlab' unless Object.const_defined?(:Gitlab)
class PreMergeChecks
DEFAULT_API_ENDPOINT = "https://gitlab.com/api/v4"
TIER_IDENTIFIER_REGEX = /tier:\d/
REQUIRED_TIER_IDENTIFIER = 'tier:3'
PREDICTIVE_PIPELINE_IDENTIFIER = 'predictive'
PIPELINE_FRESHNESS_THRESHOLD_IN_HOURS = 4
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'],
current_pipeline_id: ENV['CI_PIPELINE_ID'])
@api_endpoint = api_endpoint
@project_id = project_id
@merge_request_iid = merge_request_iid.to_i
@current_pipeline_id = current_pipeline_id.to_i
check_required_ids!
end
def execute
latest_pipeline_id = api_client.merge_request_pipelines(project_id, merge_request_iid).auto_paginate do |pipeline|
# Skip the current merge train pipeline, as we want the latest merge request pipeline
next if pipeline.id == current_pipeline_id
break pipeline.id
end
raise "Expected to have a latest pipeline but got none!" unless latest_pipeline_id
latest_pipeline = api_client.pipeline(project_id, latest_pipeline_id)
check_pipeline_for_merged_results!(latest_pipeline)
check_pipeline_freshness!(latest_pipeline)
check_pipeline_identifier!(latest_pipeline)
puts "All good for merge! 🚀"
end
private
attr_reader :api_endpoint, :project_id, :merge_request_iid, :current_pipeline_id
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!
raise 'Missing project_id' unless project_id
raise 'Missing merge_request_iid' if merge_request_iid == 0
raise 'Missing current_pipeline_id' if current_pipeline_id == 0
end
def check_pipeline_for_merged_results!(pipeline)
return if pipeline.ref == "refs/merge-requests/#{merge_request_iid}/merge"
raise "Expected to have a Merged Results pipeline but got #{pipeline.ref}!"
end
def check_pipeline_freshness!(pipeline)
hours_ago = ((Time.now - Time.parse(pipeline.created_at)) / 3600).ceil(2)
return if hours_ago < PIPELINE_FRESHNESS_THRESHOLD_IN_HOURS
raise "Expected latest pipeline to be created within the last 4 hours (it was created #{hours_ago} hours ago)!"
end
def check_pipeline_identifier!(pipeline)
if pipeline.name.match?(TIER_IDENTIFIER_REGEX)
raise <<~MSG unless pipeline.name.include?(REQUIRED_TIER_IDENTIFIER)
Expected latest pipeline to be a tier-3 pipeline!
Please ensure the MR has all the required approvals, start a new pipeline and put the MR back on the Merge Train.
MSG
elsif pipeline.name.include?(PREDICTIVE_PIPELINE_IDENTIFIER)
raise <<~MSG
Expected latest pipeline not to be a predictive pipeline!
Please ensure the MR has all the required approvals, start a new pipeline and put the MR back on the Merge Train.
MSG
end
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("-c", "--current_pipeline_id [string]", String, "Current pipeline ID") do |value|
options[:current_pipeline_id] = value
end
opts.on("-h", "--help") do
puts "Usage: merge-train-checks.rb [--project_id <PROJECT_ID>] [--merge_request_iid <MERGE_REQUEST_IID>] " \
"[--current_pipeline_id <CURRENT_PIPELINE_ID>]"
puts
puts "Examples:"
puts
puts "merge-train-checks.rb --project_id \"gitlab-org/gitlab\" --merge_request_iid \"1\" " \
"--current_pipeline_id \"2\""
exit
end
end.parse!
PreMergeChecks.new(**options).execute
end
# frozen_string_literal: true
require 'fast_spec_helper'
require_relative '../../support/webmock'
require_relative '../../../scripts/pipeline/pre_merge_checks'
RSpec.describe PreMergeChecks, time_travel_to: Time.parse('2024-05-29T08:00:00 UTC'), feature_category: :tooling do
include StubENV
let(:instance) { described_class.new }
let(:project_id) { '42' }
let(:merge_request_iid) { '1' }
let(:current_pipeline_id) { mr_pipelines[0][:id].to_s }
let(:mr_pipelines_url) { "https://gitlab.test/api/v4/projects/#{project_id}/merge_requests/#{merge_request_iid}/pipelines" }
let(:latest_mr_pipeline_ref) { "refs/merge-requests/1/merge" }
let(:latest_mr_pipeline_created_at) { "2024-05-29T07:15:00 UTC" }
let(:latest_mr_pipeline_name) { "Ruby 3.2 MR [tier:3, gdk]" }
let(:latest_mr_pipeline_short) do
{
id: 1309901620,
ref: latest_mr_pipeline_ref,
status: "success",
source: "merge_request_event",
created_at: latest_mr_pipeline_created_at
}
end
let(:latest_mr_pipeline_detailed) do
latest_mr_pipeline_short.merge(name: latest_mr_pipeline_name)
end
let(:mr_pipelines) do
[
{
id: 1309903341,
ref: "refs/merge-requests/1/train",
status: "success",
source: "merge_request_event",
created_at: "2024-05-29T07:30:00 UTC"
},
latest_mr_pipeline_short,
{
id: 1309753047,
ref: "refs/merge-requests/1/train",
status: "failed",
source: "merge_request_event",
created_at: "2024-05-29T06:30:00 UTC"
},
{
id: 1308929843,
ref: "refs/merge-requests/1/merge",
status: "success",
source: "merge_request_event",
created_at: "2024-05-29T05:30:00 UTC"
},
{
id: 1308699353,
ref: "refs/merge-requests/1/head",
status: "failed",
source: "merge_request_event",
created_at: "2024-05-29T04:30:00 UTC"
}
]
end
before do
stub_env(
'CI_API_V4_URL' => 'https://gitlab.test/api/v4',
'CI_PROJECT_ID' => project_id,
'CI_MERGE_REQUEST_IID' => merge_request_iid,
'CI_PIPELINE_ID' => current_pipeline_id
)
end
describe '#initialize' do
context 'when project_id is missing' do
let(:project_id) { nil }
it 'raises an error' do
expect { instance }.to raise_error("Missing project_id")
end
end
context 'when merge_request_iid is missing' do
let(:merge_request_iid) { nil }
it 'raises an error' do
expect { instance }.to raise_error("Missing merge_request_iid")
end
end
context 'when current_pipeline_id is missing' do
let(:current_pipeline_id) { nil }
it 'raises an error' do
expect { instance }.to raise_error("Missing current_pipeline_id")
end
end
end
describe '#execute' do
# rubocop:disable RSpec/VerifiedDoubles -- See the disclaimer above
let(:api_client) { double('Gitlab::Client') }
let(:latest_mr_pipeline) { double('pipeline', **latest_mr_pipeline_detailed) }
# We need to take some precautions when using the `gitlab` gem in this project.
#
# See https://docs.gitlab.com/ee/development/pipelines/internals.html#using-the-gitlab-ruby-gem-in-the-canonical-project.
#
before do
stub_request(:get, mr_pipelines_url).to_return(status: 200, body: mr_pipelines.to_json)
allow(instance).to receive(:api_client).and_return(api_client)
allow(api_client).to yield_pipelines(:merge_request_pipelines, mr_pipelines)
# Ensure we don't output to stdout while running tests
allow(instance).to receive(:puts)
end
def yield_pipelines(api_method, pipelines)
messages = receive_message_chain(api_method, :auto_paginate)
pipelines.inject(messages) do |stub, pipeline|
stub.and_yield(double(**pipeline))
end
end
# rubocop:enable RSpec/VerifiedDoubles
context 'when default arguments are present' do
context 'when we have a latest pipeline' do
before do
allow(api_client).to receive(:pipeline).with(project_id, mr_pipelines[1][:id]).and_return(latest_mr_pipeline)
end
context 'and it passes all the checks' do
it 'does not raise an error' do
expect { instance.execute }.not_to raise_error
end
end
context 'and it is not a merged results pipeline' do
let(:latest_mr_pipeline_ref) { "refs/merge-requests/1/head" }
it 'raises an error' do
expect { instance.execute }.to raise_error(
"Expected to have a Merged Results pipeline but got #{latest_mr_pipeline_ref}!"
)
end
end
context 'and it is not fresh enough' do
let(:latest_mr_pipeline_created_at) { "2024-05-29T03:30:00 UTC" }
it 'raises an error' do
expect { instance.execute }.to raise_error(
"Expected latest pipeline to be created within the last 4 hours (it was created 4.5 hours ago)!"
)
end
end
context 'and it is a predictive pipeline' do
let(:latest_mr_pipeline_name) { "Ruby 3.2 MR [predictive]" }
it 'raises an error' do
expect { instance.execute }
.to raise_error(/\AExpected latest pipeline not to be a predictive pipeline!/)
end
end
context 'and it is not a tier-3 pipeline' do
let(:latest_mr_pipeline_name) { "Ruby 3.2 MR [tier:2]" }
it 'raises an error' do
expect { instance.execute }
.to raise_error(/\AExpected latest pipeline to be a tier-3 pipeline!/)
end
end
context 'and it is qa-only pipeline' do
let(:latest_mr_pipeline_name) { "Ruby 3.2 MR [types:qa,qa-gdk]" }
it 'does not raise an error' do
expect { instance.execute }
.not_to raise_error
end
end
end
context 'when we do not have a latest pipeline' do
let(:mr_pipelines) do
[
{
id: 1309903341,
ref: "refs/merge-requests/1/train",
status: "success",
source: "merge_request_event",
created_at: "2024-05-29T08:29:43.472Z"
}
]
end
it 'raises an error' do
expect { instance.execute }.to raise_error("Expected to have a latest pipeline but got none!")
end
end
end
end
end
...@@ -24,7 +24,7 @@ def webmock_allowed_hosts ...@@ -24,7 +24,7 @@ def webmock_allowed_hosts
hosts << Gitlab.config.webpack.dev_server.host hosts << Gitlab.config.webpack.dev_server.host
end end
if ViteRuby.env['VITE_ENABLED'] == "true" if Object.const_defined?(:ViteRuby) && ViteRuby.env['VITE_ENABLED'] == "true"
hosts << ViteRuby.instance.config.host hosts << ViteRuby.instance.config.host
hosts << ViteRuby.env['VITE_HMR_HOST'] hosts << ViteRuby.env['VITE_HMR_HOST']
end end
......
0% 加载中 .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册