Commit f2c9081c authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'patch-211' into 'master'

Add CI_OPEN_MERGE_REQUESTS environment variable

See merge request gitlab-org/gitlab!38673
parents 86a35180 6122ba8d
...@@ -7,6 +7,7 @@ module Ci ...@@ -7,6 +7,7 @@ module Ci
include Importable include Importable
include AfterCommitQueue include AfterCommitQueue
include Presentable include Presentable
include Gitlab::Allowable
include Gitlab::OptimisticLocking include Gitlab::OptimisticLocking
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
include AtomicInternalId include AtomicInternalId
...@@ -16,6 +17,8 @@ module Ci ...@@ -16,6 +17,8 @@ module Ci
include FromUnion include FromUnion
include UpdatedAtFilterable include UpdatedAtFilterable
MAX_OPEN_MERGE_REQUESTS_REFS = 4
PROJECT_ROUTE_AND_NAMESPACE_ROUTE = { PROJECT_ROUTE_AND_NAMESPACE_ROUTE = {
project: [:project_feature, :route, { namespace: :route }] project: [:project_feature, :route, { namespace: :route }]
}.freeze }.freeze
...@@ -801,6 +804,10 @@ module Ci ...@@ -801,6 +804,10 @@ module Ci
variables.concat(merge_request.predefined_variables) variables.concat(merge_request.predefined_variables)
end end
if Gitlab::Ci::Features.pipeline_open_merge_requests?(project) && open_merge_requests_refs.any?
variables.append(key: 'CI_OPEN_MERGE_REQUESTS', value: open_merge_requests_refs.join(','))
end
variables.append(key: 'CI_KUBERNETES_ACTIVE', value: 'true') if has_kubernetes_active? variables.append(key: 'CI_KUBERNETES_ACTIVE', value: 'true') if has_kubernetes_active?
variables.append(key: 'CI_DEPLOY_FREEZE', value: 'true') if freeze_period? variables.append(key: 'CI_DEPLOY_FREEZE', value: 'true') if freeze_period?
...@@ -867,6 +874,36 @@ module Ci ...@@ -867,6 +874,36 @@ module Ci
all_merge_requests.order(id: :desc) all_merge_requests.order(id: :desc)
end end
# This returns a list of MRs that point
# to the same source project/branch
def related_merge_requests
if merge_request?
# We look for all other MRs that this branch might be pointing to
MergeRequest.where(
source_project_id: merge_request.source_project_id,
source_branch: merge_request.source_branch)
else
MergeRequest.where(
source_project_id: project_id,
source_branch: ref)
end
end
# We cannot use `all_merge_requests`, due to race condition
# This returns a list of at most 4 open MRs
def open_merge_requests_refs
strong_memoize(:open_merge_requests_refs) do
# We ensure that triggering user can actually read the pipeline
related_merge_requests
.opened
.limit(MAX_OPEN_MERGE_REQUESTS_REFS)
.order(id: :desc)
.preload(:target_project)
.select { |mr| can?(user, :read_merge_request, mr) }
.map { |mr| mr.to_reference(project, full: true) }
end
end
def same_family_pipeline_ids def same_family_pipeline_ids
::Gitlab::Ci::PipelineObjectHierarchy.new( ::Gitlab::Ci::PipelineObjectHierarchy.new(
self.class.where(id: root_ancestor), options: { same_project: true } self.class.where(id: root_ancestor), options: { same_project: true }
......
---
title: Add CI_OPEN_MERGE_REQUESTS environment variable
merge_request: 38673
author: Ben Bodenmiller @bbodenmiller
type: added
---
name: ci_pipeline_open_merge_requests
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/38673
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/292727
group: group::memory
type: development
default_enabled: false
...@@ -68,6 +68,7 @@ Kubernetes-specific environment variables are detailed in the ...@@ -68,6 +68,7 @@ Kubernetes-specific environment variables are detailed in the
| `CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME` | 12.3 | all | The target branch name of the pull request if [the pipelines are for external pull requests](../ci_cd_for_external_repos/index.md#pipelines-for-external-pull-requests). Available only if `only: [external_pull_requests]` or [`rules`](../yaml/README.md#rules) syntax is used and the pull request is open. | | `CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME` | 12.3 | all | The target branch name of the pull request if [the pipelines are for external pull requests](../ci_cd_for_external_repos/index.md#pipelines-for-external-pull-requests). Available only if `only: [external_pull_requests]` or [`rules`](../yaml/README.md#rules) syntax is used and the pull request is open. |
| `CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_SHA` | 12.3 | all | The HEAD SHA of the target branch of the pull request if [the pipelines are for external pull requests](../ci_cd_for_external_repos/index.md#pipelines-for-external-pull-requests). Available only if `only: [external_pull_requests]` or [`rules`](../yaml/README.md#rules) syntax is used and the pull request is open. | | `CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_SHA` | 12.3 | all | The HEAD SHA of the target branch of the pull request if [the pipelines are for external pull requests](../ci_cd_for_external_repos/index.md#pipelines-for-external-pull-requests). Available only if `only: [external_pull_requests]` or [`rules`](../yaml/README.md#rules) syntax is used and the pull request is open. |
| `CI_HAS_OPEN_REQUIREMENTS` | 13.1 | all | Included with the value `true` only if the pipeline's project has any open [requirements](../../user/project/requirements/index.md). Not included if there are no open requirements for the pipeline's project. | | `CI_HAS_OPEN_REQUIREMENTS` | 13.1 | all | Included with the value `true` only if the pipeline's project has any open [requirements](../../user/project/requirements/index.md). Not included if there are no open requirements for the pipeline's project. |
| `CI_OPEN_MERGE_REQUESTS` | 13.7 | all | Contains a comma-delimited list of up to 4 Merge Requests from the current source project and branch in the form `gitlab-org/gitlab!333,gitlab-org/gitlab-foss!11` |
| `CI_JOB_ID` | 9.0 | all | The unique ID of the current job that GitLab CI/CD uses internally | | `CI_JOB_ID` | 9.0 | all | The unique ID of the current job that GitLab CI/CD uses internally |
| `CI_JOB_IMAGE` | 12.9 | 12.9 | The name of the image running the CI job | | `CI_JOB_IMAGE` | 12.9 | 12.9 | The name of the image running the CI job |
| `CI_JOB_MANUAL` | 8.12 | all | The flag to indicate that job was manually started | | `CI_JOB_MANUAL` | 8.12 | all | The flag to indicate that job was manually started |
......
...@@ -55,6 +55,10 @@ module Gitlab ...@@ -55,6 +55,10 @@ module Gitlab
::Feature.enabled?(:ci_trace_log_invalid_chunks, project, type: :ops, default_enabled: false) ::Feature.enabled?(:ci_trace_log_invalid_chunks, project, type: :ops, default_enabled: false)
end end
def self.pipeline_open_merge_requests?(project)
::Feature.enabled?(:ci_pipeline_open_merge_requests, project, default_enabled: false)
end
def self.seed_block_run_before_workflow_rules_enabled?(project) def self.seed_block_run_before_workflow_rules_enabled?(project)
::Feature.enabled?(:ci_seed_block_run_before_workflow_rules, project, default_enabled: true) ::Feature.enabled?(:ci_seed_block_run_before_workflow_rules, project, default_enabled: true)
end end
......
...@@ -795,11 +795,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do ...@@ -795,11 +795,7 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
] ]
end end
context 'when pipeline is merge request' do context 'when merge request is present' do
let(:pipeline) do
create(:ci_pipeline, :detached_merge_request_pipeline, merge_request: merge_request)
end
let(:merge_request) do let(:merge_request) do
create(:merge_request, create(:merge_request,
source_project: project, source_project: project,
...@@ -815,57 +811,106 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do ...@@ -815,57 +811,106 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
let(:milestone) { create(:milestone, project: project) } let(:milestone) { create(:milestone, project: project) }
let(:labels) { create_list(:label, 2) } let(:labels) { create_list(:label, 2) }
it 'exposes merge request pipeline variables' do context 'when pipeline for merge request is created' do
expect(subject.to_hash) let(:pipeline) do
.to include( create(:ci_pipeline, :detached_merge_request_pipeline,
'CI_MERGE_REQUEST_ID' => merge_request.id.to_s, ci_ref_presence: false,
'CI_MERGE_REQUEST_IID' => merge_request.iid.to_s, user: user,
'CI_MERGE_REQUEST_REF_PATH' => merge_request.ref_path.to_s, merge_request: merge_request)
'CI_MERGE_REQUEST_PROJECT_ID' => merge_request.project.id.to_s, end
'CI_MERGE_REQUEST_PROJECT_PATH' => merge_request.project.full_path,
'CI_MERGE_REQUEST_PROJECT_URL' => merge_request.project.web_url, before do
'CI_MERGE_REQUEST_TARGET_BRANCH_NAME' => merge_request.target_branch.to_s, project.add_developer(user)
'CI_MERGE_REQUEST_TARGET_BRANCH_SHA' => '', end
'CI_MERGE_REQUEST_SOURCE_PROJECT_ID' => merge_request.source_project.id.to_s,
'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH' => merge_request.source_project.full_path, it 'exposes merge request pipeline variables' do
'CI_MERGE_REQUEST_SOURCE_PROJECT_URL' => merge_request.source_project.web_url, expect(subject.to_hash)
'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s, .to include(
'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' => '', 'CI_MERGE_REQUEST_ID' => merge_request.id.to_s,
'CI_MERGE_REQUEST_TITLE' => merge_request.title, 'CI_MERGE_REQUEST_IID' => merge_request.iid.to_s,
'CI_MERGE_REQUEST_ASSIGNEES' => merge_request.assignee_username_list, 'CI_MERGE_REQUEST_REF_PATH' => merge_request.ref_path.to_s,
'CI_MERGE_REQUEST_MILESTONE' => milestone.title, 'CI_MERGE_REQUEST_PROJECT_ID' => merge_request.project.id.to_s,
'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).sort.join(','), 'CI_MERGE_REQUEST_PROJECT_PATH' => merge_request.project.full_path,
'CI_MERGE_REQUEST_EVENT_TYPE' => 'detached') 'CI_MERGE_REQUEST_PROJECT_URL' => merge_request.project.web_url,
end 'CI_MERGE_REQUEST_TARGET_BRANCH_NAME' => merge_request.target_branch.to_s,
'CI_MERGE_REQUEST_TARGET_BRANCH_SHA' => '',
it 'exposes diff variables' do 'CI_MERGE_REQUEST_SOURCE_PROJECT_ID' => merge_request.source_project.id.to_s,
expect(subject.to_hash) 'CI_MERGE_REQUEST_SOURCE_PROJECT_PATH' => merge_request.source_project.full_path,
.to include( 'CI_MERGE_REQUEST_SOURCE_PROJECT_URL' => merge_request.source_project.web_url,
'CI_MERGE_REQUEST_DIFF_ID' => merge_request.merge_request_diff.id.to_s, 'CI_MERGE_REQUEST_SOURCE_BRANCH_NAME' => merge_request.source_branch.to_s,
'CI_MERGE_REQUEST_DIFF_BASE_SHA' => merge_request.merge_request_diff.base_commit_sha) 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA' => '',
end 'CI_MERGE_REQUEST_TITLE' => merge_request.title,
'CI_MERGE_REQUEST_ASSIGNEES' => merge_request.assignee_username_list,
'CI_MERGE_REQUEST_MILESTONE' => milestone.title,
'CI_MERGE_REQUEST_LABELS' => labels.map(&:title).sort.join(','),
'CI_MERGE_REQUEST_EVENT_TYPE' => 'detached',
'CI_OPEN_MERGE_REQUESTS' => merge_request.to_reference(full: true))
end
it 'exposes diff variables' do
expect(subject.to_hash)
.to include(
'CI_MERGE_REQUEST_DIFF_ID' => merge_request.merge_request_diff.id.to_s,
'CI_MERGE_REQUEST_DIFF_BASE_SHA' => merge_request.merge_request_diff.base_commit_sha)
end
context 'without assignee' do context 'without assignee' do
let(:assignees) { [] } let(:assignees) { [] }
it 'does not expose assignee variable' do it 'does not expose assignee variable' do
expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_ASSIGNEES') expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_ASSIGNEES')
end
end end
end
context 'without milestone' do context 'without milestone' do
let(:milestone) { nil } let(:milestone) { nil }
it 'does not expose milestone variable' do it 'does not expose milestone variable' do
expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_MILESTONE') expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_MILESTONE')
end
end
context 'without labels' do
let(:labels) { [] }
it 'does not expose labels variable' do
expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_LABELS')
end
end end
end end
context 'without labels' do context 'when pipeline on branch is created' do
let(:labels) { [] } let(:pipeline) do
create(:ci_pipeline, project: project, user: user, ref: 'feature')
end
context 'when a merge request is created' do
before do
merge_request
end
context 'when user has access to project' do
before do
project.add_developer(user)
end
it 'merge request references are returned matching the pipeline' do
expect(subject.to_hash).to include(
'CI_OPEN_MERGE_REQUESTS' => merge_request.to_reference(full: true))
end
end
context 'when user does not have access to project' do
it 'CI_OPEN_MERGE_REQUESTS is not returned' do
expect(subject.to_hash).not_to have_key('CI_OPEN_MERGE_REQUESTS')
end
end
end
it 'does not expose labels variable' do context 'when no a merge request is created' do
expect(subject.to_hash.keys).not_to include('CI_MERGE_REQUEST_LABELS') it 'CI_OPEN_MERGE_REQUESTS is not returned' do
expect(subject.to_hash).not_to have_key('CI_OPEN_MERGE_REQUESTS')
end
end end
end end
...@@ -2862,6 +2907,93 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do ...@@ -2862,6 +2907,93 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
end end
end end
describe '#related_merge_requests' do
let(:project) { create(:project, :repository) }
let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master') }
let(:other_merge_request) { create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'stable') }
let(:branch_pipeline) { create(:ci_pipeline, project: project, ref: 'feature') }
let(:merge_pipeline) { create(:ci_pipeline, :detached_merge_request_pipeline, merge_request: merge_request) }
context 'for a branch pipeline' do
subject { branch_pipeline.related_merge_requests }
it 'when no merge request is created' do
is_expected.to be_empty
end
it 'when another merge requests are created' do
merge_request
other_merge_request
is_expected.to contain_exactly(merge_request, other_merge_request)
end
end
context 'for a merge pipeline' do
subject { merge_pipeline.related_merge_requests }
it 'when only merge pipeline is created' do
merge_pipeline
is_expected.to contain_exactly(merge_request)
end
it 'when a merge request is created' do
merge_pipeline
other_merge_request
is_expected.to contain_exactly(merge_request, other_merge_request)
end
end
end
describe '#open_merge_requests_refs' do
let(:project) { create(:project) }
let(:user) { create(:user) }
let!(:pipeline) { create(:ci_pipeline, user: user, project: project, ref: 'feature') }
let!(:merge_request) { create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master') }
subject { pipeline.open_merge_requests_refs }
context 'when user is a developer' do
before do
project.add_developer(user)
end
it 'returns open merge requests' do
is_expected.to eq([merge_request.to_reference(full: true)])
end
it 'does not return closed merge requests' do
merge_request.close!
is_expected.to be_empty
end
context 'limits amount of returned merge requests' do
let!(:other_merge_requests) do
Array.new(4) do |idx|
create(:merge_request, source_project: project, source_branch: 'feature', target_branch: "master-#{idx}")
end
end
let(:other_merge_requests_refs) do
other_merge_requests.map { |mr| mr.to_reference(full: true) }
end
it 'returns only last 4 in a reverse order' do
is_expected.to eq(other_merge_requests_refs.reverse)
end
end
end
context 'when user does not have permissions' do
it 'does not return any merge requests' do
is_expected.to be_empty
end
end
end
describe '#same_family_pipeline_ids' do describe '#same_family_pipeline_ids' do
subject { pipeline.same_family_pipeline_ids.map(&:id) } subject { pipeline.same_family_pipeline_ids.map(&:id) }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::CreatePipelineService do
context 'merge requests handling' do
let_it_be(:project) { create(:project, :repository) }
let_it_be(:user) { project.owner }
let(:ref) { 'refs/heads/feature' }
let(:source) { :push }
let(:service) { described_class.new(project, user, { ref: ref }) }
let(:pipeline) { service.execute(source) }
before do
stub_ci_pipeline_yaml_file <<-EOS
workflow:
rules:
# do not create pipelines if merge requests are opened
- if: $CI_OPEN_MERGE_REQUESTS
when: never
- if: $CI_COMMIT_BRANCH
rspec:
script: echo Hello World
EOS
end
context 'when pushing a change' do
context 'when a merge request already exists' do
let!(:merge_request) do
create(:merge_request,
source_project: project,
source_branch: 'feature',
target_project: project,
target_branch: 'master')
end
it 'does not create a pipeline' do
expect(pipeline).not_to be_persisted
end
end
context 'when no merge request exists' do
it 'does create a pipeline' do
expect(pipeline.errors).to be_empty
expect(pipeline).to be_persisted
end
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