Commit 0bba522f authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch 'backstage/gb/refactor-pipeline-create-service' into 'master'

Refactor a service responsible for creating a pipeline

Closes #37563 and #34415

See merge request gitlab-org/gitlab-ce!14482
parents 02424962 26e73c2e
...@@ -434,7 +434,7 @@ module Ci ...@@ -434,7 +434,7 @@ module Ci
def update_duration def update_duration
return unless started_at return unless started_at
self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self) self.duration = Gitlab::Ci::Pipeline::Duration.from_pipeline(self)
end end
def execute_hooks def execute_hooks
......
...@@ -2,110 +2,55 @@ module Ci ...@@ -2,110 +2,55 @@ module Ci
class CreatePipelineService < BaseService class CreatePipelineService < BaseService
attr_reader :pipeline attr_reader :pipeline
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil) SEQUENCE = [Gitlab::Ci::Pipeline::Chain::Validate::Abilities,
Gitlab::Ci::Pipeline::Chain::Validate::Repository,
Gitlab::Ci::Pipeline::Chain::Validate::Config,
Gitlab::Ci::Pipeline::Chain::Skip,
Gitlab::Ci::Pipeline::Chain::Create].freeze
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, &block)
@pipeline = Ci::Pipeline.new( @pipeline = Ci::Pipeline.new(
source: source, source: source,
project: project, project: project,
ref: ref, ref: ref,
sha: sha, sha: sha,
before_sha: before_sha, before_sha: before_sha,
tag: tag?, tag: tag_exists?,
trigger_requests: Array(trigger_request), trigger_requests: Array(trigger_request),
user: current_user, user: current_user,
pipeline_schedule: schedule, pipeline_schedule: schedule,
protected: project.protected_for?(ref) protected: project.protected_for?(ref)
) )
result = validate_project_and_git_items || command = OpenStruct.new(ignore_skip_ci: ignore_skip_ci,
validate_pipeline(ignore_skip_ci: ignore_skip_ci, save_incompleted: save_on_errors,
save_on_errors: save_on_errors) seeds_block: block,
project: project,
current_user: current_user)
return result if result sequence = Gitlab::Ci::Pipeline::Chain::Sequence
.new(pipeline, command, SEQUENCE)
begin sequence.build! do |pipeline, sequence|
Ci::Pipeline.transaction do update_merge_requests_head_pipeline if pipeline.persisted?
pipeline.save!
yield(pipeline) if block_given? if sequence.complete?
cancel_pending_pipelines if project.auto_cancel_pending_pipelines?
pipeline_created_counter.increment(source: source)
Ci::CreatePipelineStagesService pipeline.process!
.new(project, current_user)
.execute(pipeline)
end end
rescue ActiveRecord::RecordInvalid => e
return error("Failed to persist the pipeline: #{e}")
end end
update_merge_requests_head_pipeline
cancel_pending_pipelines if project.auto_cancel_pending_pipelines?
pipeline_created_counter.increment(source: source)
pipeline.tap(&:process!)
end end
private private
def validate_project_and_git_items def commit
unless project.builds_enabled? @commit ||= project.commit(origin_sha || origin_ref)
return error('Pipeline is disabled')
end
unless allowed_to_trigger_pipeline?
if can?(current_user, :create_pipeline, project)
return error("Insufficient permissions for protected ref '#{ref}'")
else
return error('Insufficient permissions to create a new pipeline')
end
end
unless branch? || tag?
return error('Reference not found')
end
unless commit
return error('Commit not found')
end
end
def validate_pipeline(ignore_skip_ci:, save_on_errors:)
unless pipeline.config_processor
unless pipeline.ci_yaml_file
return error("Missing #{pipeline.ci_yaml_file_path} file")
end
return error(pipeline.yaml_errors, save: save_on_errors)
end
if !ignore_skip_ci && skip_ci?
pipeline.skip if save_on_errors
return pipeline
end
unless pipeline.has_stage_seeds?
return error('No stages / jobs for this pipeline.')
end
end
def allowed_to_trigger_pipeline?
if current_user
allowed_to_create?
else # legacy triggers don't have a corresponding user
!project.protected_for?(ref)
end
end end
def allowed_to_create? def sha
return unless can?(current_user, :create_pipeline, project) commit.try(:id)
access = Gitlab::UserAccess.new(current_user, project: project)
if branch?
access.can_update_branch?(ref)
elsif tag?
access.can_create_tag?(ref)
else
true # Allow it for now and we'll reject when we check ref existence
end
end end
def update_merge_requests_head_pipeline def update_merge_requests_head_pipeline
...@@ -115,11 +60,6 @@ module Ci ...@@ -115,11 +60,6 @@ module Ci
.update_all(head_pipeline_id: @pipeline.id) .update_all(head_pipeline_id: @pipeline.id)
end end
def skip_ci?
return false unless pipeline.git_commit_message
pipeline.git_commit_message =~ /\[(ci[ _-]skip|skip[ _-]ci)\]/i
end
def cancel_pending_pipelines def cancel_pending_pipelines
Gitlab::OptimisticLocking.retry_lock(auto_cancelable_pipelines) do |cancelables| Gitlab::OptimisticLocking.retry_lock(auto_cancelable_pipelines) do |cancelables|
cancelables.find_each do |cancelable| cancelables.find_each do |cancelable|
...@@ -136,14 +76,6 @@ module Ci ...@@ -136,14 +76,6 @@ module Ci
.created_or_pending .created_or_pending
end end
def commit
@commit ||= project.commit(origin_sha || origin_ref)
end
def sha
commit.try(:id)
end
def before_sha def before_sha
params[:checkout_sha] || params[:before] || Gitlab::Git::BLANK_SHA params[:checkout_sha] || params[:before] || Gitlab::Git::BLANK_SHA
end end
...@@ -156,41 +88,17 @@ module Ci ...@@ -156,41 +88,17 @@ module Ci
params[:ref] params[:ref]
end end
def branch? def tag_exists?
return @is_branch if defined?(@is_branch) project.repository.tag_exists?(ref)
@is_branch =
project.repository.ref_exists?(Gitlab::Git::BRANCH_REF_PREFIX + ref)
end
def tag?
return @is_tag if defined?(@is_tag)
@is_tag =
project.repository.ref_exists?(Gitlab::Git::TAG_REF_PREFIX + ref)
end end
def ref def ref
@ref ||= Gitlab::Git.ref_name(origin_ref) @ref ||= Gitlab::Git.ref_name(origin_ref)
end end
def valid_sha?
origin_sha && origin_sha != Gitlab::Git::BLANK_SHA
end
def error(message, save: false)
pipeline.tap do
pipeline.errors.add(:base, message)
if save
pipeline.drop
update_merge_requests_head_pipeline
end
end
end
def pipeline_created_counter def pipeline_created_counter
@pipeline_created_counter ||= Gitlab::Metrics.counter(:pipelines_created_total, "Counter of pipelines created") @pipeline_created_counter ||= Gitlab::Metrics
.counter(:pipelines_created_total, "Counter of pipelines created")
end end
end end
end end
module Gitlab
module Ci
module Pipeline
module Chain
class Base
attr_reader :pipeline, :project, :current_user
def initialize(pipeline, command)
@pipeline = pipeline
@command = command
@project = command.project
@current_user = command.current_user
end
def perform!
raise NotImplementedError
end
def break?
raise NotImplementedError
end
end
end
end
end
end
module Gitlab
module Ci
module Pipeline
module Chain
class Create < Chain::Base
include Chain::Helpers
def perform!
::Ci::Pipeline.transaction do
pipeline.save!
@command.seeds_block&.call(pipeline)
::Ci::CreatePipelineStagesService
.new(project, current_user)
.execute(pipeline)
end
rescue ActiveRecord::RecordInvalid => e
error("Failed to persist the pipeline: #{e}")
end
def break?
!pipeline.persisted?
end
end
end
end
end
end
module Gitlab
module Ci
module Pipeline
module Chain
module Helpers
def branch_exists?
return @is_branch if defined?(@is_branch)
@is_branch = project.repository.branch_exists?(pipeline.ref)
end
def tag_exists?
return @is_tag if defined?(@is_tag)
@is_tag = project.repository.tag_exists?(pipeline.ref)
end
def error(message)
pipeline.errors.add(:base, message)
end
end
end
end
end
end
module Gitlab
module Ci
module Pipeline
module Chain
class Sequence
def initialize(pipeline, command, sequence)
@pipeline = pipeline
@completed = []
@sequence = sequence.map do |chain|
chain.new(pipeline, command)
end
end
def build!
@sequence.each do |step|
step.perform!
break if step.break?
@completed << step
end
@pipeline.tap do
yield @pipeline, self if block_given?
end
end
def complete?
@completed.size == @sequence.size
end
end
end
end
end
end
module Gitlab
module Ci
module Pipeline
module Chain
class Skip < Chain::Base
SKIP_PATTERN = /\[(ci[ _-]skip|skip[ _-]ci)\]/i
def perform!
if skipped?
@pipeline.skip if @command.save_incompleted
end
end
def skipped?
!@command.ignore_skip_ci && commit_message_skips_ci?
end
def break?
skipped?
end
private
def commit_message_skips_ci?
return false unless @pipeline.git_commit_message
@skipped ||= !!(@pipeline.git_commit_message =~ SKIP_PATTERN)
end
end
end
end
end
end
module Gitlab
module Ci
module Pipeline
module Chain
module Validate
class Abilities < Chain::Base
include Gitlab::Allowable
include Chain::Helpers
def perform!
unless project.builds_enabled?
return error('Pipelines are disabled!')
end
unless allowed_to_trigger_pipeline?
if can?(current_user, :create_pipeline, project)
return error("Insufficient permissions for protected ref '#{pipeline.ref}'")
else
return error('Insufficient permissions to create a new pipeline')
end
end
end
def break?
@pipeline.errors.any?
end
def allowed_to_trigger_pipeline?
if current_user
allowed_to_create?
else # legacy triggers don't have a corresponding user
!project.protected_for?(@pipeline.ref)
end
end
def allowed_to_create?
return unless can?(current_user, :create_pipeline, project)
access = Gitlab::UserAccess.new(current_user, project: project)
if branch_exists?
access.can_update_branch?(@pipeline.ref)
elsif tag_exists?
access.can_create_tag?(@pipeline.ref)
else
true # Allow it for now and we'll reject when we check ref existence
end
end
end
end
end
end
end
end
module Gitlab
module Ci
module Pipeline
module Chain
module Validate
class Config < Chain::Base
include Chain::Helpers
def perform!
unless @pipeline.config_processor
unless @pipeline.ci_yaml_file
return error("Missing #{@pipeline.ci_yaml_file_path} file")
end
if @command.save_incompleted && @pipeline.has_yaml_errors?
@pipeline.drop
end
return error(@pipeline.yaml_errors)
end
unless @pipeline.has_stage_seeds?
return error('No stages / jobs for this pipeline.')
end
end
def break?
@pipeline.errors.any? || @pipeline.persisted?
end
end
end
end
end
end
end
module Gitlab
module Ci
module Pipeline
module Chain
module Validate
class Repository < Chain::Base
include Chain::Helpers
def perform!
unless branch_exists? || tag_exists?
return error('Reference not found')
end
## TODO, we check commit in the service, that is why
# there is no repository access here.
#
unless pipeline.sha
return error('Commit not found')
end
end
def break?
@pipeline.errors.any?
end
end
end
end
end
end
end
module Gitlab
module Ci
module Pipeline
# # Introduction - total running time
#
# The problem this module is trying to solve is finding the total running
# time amongst all the jobs, excluding retries and pending (queue) time.
# We could reduce this problem down to finding the union of periods.
#
# So each job would be represented as a `Period`, which consists of
# `Period#first` as when the job started and `Period#last` as when the
# job was finished. A simple example here would be:
#
# * A (1, 3)
# * B (2, 4)
# * C (6, 7)
#
# Here A begins from 1, and ends to 3. B begins from 2, and ends to 4.
# C begins from 6, and ends to 7. Visually it could be viewed as:
#
# 0 1 2 3 4 5 6 7
# AAAAAAA
# BBBBBBB
# CCCC
#
# The union of A, B, and C would be (1, 4) and (6, 7), therefore the
# total running time should be:
#
# (4 - 1) + (7 - 6) => 4
#
# # The Algorithm
#
# The algorithm used here for union would be described as follow.
# First we make sure that all periods are sorted by `Period#first`.
# Then we try to merge periods by iterating through the first period
# to the last period. The goal would be merging all overlapped periods
# so that in the end all the periods are discrete. When all periods
# are discrete, we're free to just sum all the periods to get real
# running time.
#
# Here we begin from A, and compare it to B. We could find that
# before A ends, B already started. That is `B.first <= A.last`
# that is `2 <= 3` which means A and B are overlapping!
#
# When we found that two periods are overlapping, we would need to merge
# them into a new period and disregard the old periods. To make a new
# period, we take `A.first` as the new first because remember? we sorted
# them, so `A.first` must be smaller or equal to `B.first`. And we take
# `[A.last, B.last].max` as the new last because we want whoever ended
# later. This could be broken into two cases:
#
# 0 1 2 3 4
# AAAAAAA
# BBBBBBB
#
# Or:
#
# 0 1 2 3 4
# AAAAAAAAAA
# BBBB
#
# So that we need to take whoever ends later. Back to our example,
# after merging and discard A and B it could be visually viewed as:
#
# 0 1 2 3 4 5 6 7
# DDDDDDDDDD
# CCCC
#
# Now we could go on and compare the newly created D and the old C.
# We could figure out that D and C are not overlapping by checking
# `C.first <= D.last` is `false`. Therefore we need to keep both C
# and D. The example would end here because there are no more jobs.
#
# After having the union of all periods, we just need to sum the length
# of all periods to get total time.
#
# (4 - 1) + (7 - 6) => 4
#
# That is 4 is the answer in the example.
module Duration
extend self
Period = Struct.new(:first, :last) do
def duration
last - first
end
end
def from_pipeline(pipeline)
status = %w[success failed running canceled]
builds = pipeline.builds.latest
.where(status: status).where.not(started_at: nil).order(:started_at)
from_builds(builds)
end
def from_builds(builds)
now = Time.now
periods = builds.map do |b|
Period.new(b.started_at, b.finished_at || now)
end
from_periods(periods)
end
# periods should be sorted by `first`
def from_periods(periods)
process_duration(process_periods(periods))
end
private
def process_periods(periods)
return periods if periods.empty?
periods.drop(1).inject([periods.first]) do |result, current|
previous = result.last
if overlap?(previous, current)
result[-1] = merge(previous, current)
result
else
result << current
end
end
end
def overlap?(previous, current)
current.first <= previous.last
end
def merge(previous, current)
Period.new(previous.first, [previous.last, current.last].max)
end
def process_duration(periods)
periods.sum(&:duration)
end
end
end
end
end
module Gitlab
module Ci
# # Introduction - total running time
#
# The problem this module is trying to solve is finding the total running
# time amongst all the jobs, excluding retries and pending (queue) time.
# We could reduce this problem down to finding the union of periods.
#
# So each job would be represented as a `Period`, which consists of
# `Period#first` as when the job started and `Period#last` as when the
# job was finished. A simple example here would be:
#
# * A (1, 3)
# * B (2, 4)
# * C (6, 7)
#
# Here A begins from 1, and ends to 3. B begins from 2, and ends to 4.
# C begins from 6, and ends to 7. Visually it could be viewed as:
#
# 0 1 2 3 4 5 6 7
# AAAAAAA
# BBBBBBB
# CCCC
#
# The union of A, B, and C would be (1, 4) and (6, 7), therefore the
# total running time should be:
#
# (4 - 1) + (7 - 6) => 4
#
# # The Algorithm
#
# The algorithm used here for union would be described as follow.
# First we make sure that all periods are sorted by `Period#first`.
# Then we try to merge periods by iterating through the first period
# to the last period. The goal would be merging all overlapped periods
# so that in the end all the periods are discrete. When all periods
# are discrete, we're free to just sum all the periods to get real
# running time.
#
# Here we begin from A, and compare it to B. We could find that
# before A ends, B already started. That is `B.first <= A.last`
# that is `2 <= 3` which means A and B are overlapping!
#
# When we found that two periods are overlapping, we would need to merge
# them into a new period and disregard the old periods. To make a new
# period, we take `A.first` as the new first because remember? we sorted
# them, so `A.first` must be smaller or equal to `B.first`. And we take
# `[A.last, B.last].max` as the new last because we want whoever ended
# later. This could be broken into two cases:
#
# 0 1 2 3 4
# AAAAAAA
# BBBBBBB
#
# Or:
#
# 0 1 2 3 4
# AAAAAAAAAA
# BBBB
#
# So that we need to take whoever ends later. Back to our example,
# after merging and discard A and B it could be visually viewed as:
#
# 0 1 2 3 4 5 6 7
# DDDDDDDDDD
# CCCC
#
# Now we could go on and compare the newly created D and the old C.
# We could figure out that D and C are not overlapping by checking
# `C.first <= D.last` is `false`. Therefore we need to keep both C
# and D. The example would end here because there are no more jobs.
#
# After having the union of all periods, we just need to sum the length
# of all periods to get total time.
#
# (4 - 1) + (7 - 6) => 4
#
# That is 4 is the answer in the example.
module PipelineDuration
extend self
Period = Struct.new(:first, :last) do
def duration
last - first
end
end
def from_pipeline(pipeline)
status = %w[success failed running canceled]
builds = pipeline.builds.latest
.where(status: status).where.not(started_at: nil).order(:started_at)
from_builds(builds)
end
def from_builds(builds)
now = Time.now
periods = builds.map do |b|
Period.new(b.started_at, b.finished_at || now)
end
from_periods(periods)
end
# periods should be sorted by `first`
def from_periods(periods)
process_duration(process_periods(periods))
end
private
def process_periods(periods)
return periods if periods.empty?
periods.drop(1).inject([periods.first]) do |result, current|
previous = result.last
if overlap?(previous, current)
result[-1] = merge(previous, current)
result
else
result << current
end
end
end
def overlap?(previous, current)
current.first <= previous.last
end
def merge(previous, current)
Period.new(previous.first, [previous.last, current.last].max)
end
def process_duration(periods)
periods.sum(&:duration)
end
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Pipeline::Chain::Create do
set(:project) { create(:project) }
set(:user) { create(:user) }
let(:pipeline) do
build(:ci_pipeline_with_one_job, project: project,
ref: 'master')
end
let(:command) do
double('command', project: project,
current_user: user,
seeds_block: nil)
end
let(:step) { described_class.new(pipeline, command) }
before do
step.perform!
end
context 'when pipeline is ready to be saved' do
it 'saves a pipeline' do
expect(pipeline).to be_persisted
end
it 'does not break the chain' do
expect(step.break?).to be false
end
it 'creates stages' do
expect(pipeline.reload.stages).to be_one
end
end
context 'when pipeline has validation errors' do
let(:pipeline) do
build(:ci_pipeline, project: project, ref: nil)
end
it 'breaks the chain' do
expect(step.break?).to be true
end
it 'appends validation error' do
expect(pipeline.errors.to_a)
.to include /Failed to persist the pipeline/
end
end
context 'when there is a seed block present' do
let(:seeds) { spy('pipeline seeds') }
let(:command) do
double('command', project: project,
current_user: user,
seeds_block: seeds)
end
it 'executes the block' do
expect(seeds).to have_received(:call).with(pipeline)
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Pipeline::Chain::Sequence do
set(:project) { create(:project) }
set(:user) { create(:user) }
let(:pipeline) { build_stubbed(:ci_pipeline) }
let(:command) { double('command' ) }
let(:first_step) { spy('first step') }
let(:second_step) { spy('second step') }
let(:sequence) { [first_step, second_step] }
subject do
described_class.new(pipeline, command, sequence)
end
context 'when one of steps breaks the chain' do
before do
allow(first_step).to receive(:break?).and_return(true)
end
it 'does not process the second step' do
subject.build! do |pipeline, sequence|
expect(sequence).not_to be_complete
end
expect(second_step).not_to have_received(:perform!)
end
it 'returns a pipeline object' do
expect(subject.build!).to eq pipeline
end
end
context 'when all chains are executed correctly' do
before do
sequence.each do |step|
allow(step).to receive(:break?).and_return(false)
end
end
it 'iterates through entire sequence' do
subject.build! do |pipeline, sequence|
expect(sequence).to be_complete
end
expect(first_step).to have_received(:perform!)
expect(second_step).to have_received(:perform!)
end
it 'returns a pipeline object' do
expect(subject.build!).to eq pipeline
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Pipeline::Chain::Skip do
set(:project) { create(:project) }
set(:user) { create(:user) }
set(:pipeline) { create(:ci_pipeline, project: project) }
let(:command) do
double('command', project: project,
current_user: user,
ignore_skip_ci: false,
save_incompleted: true)
end
let(:step) { described_class.new(pipeline, command) }
context 'when pipeline has been skipped by a user' do
before do
allow(pipeline).to receive(:git_commit_message)
.and_return('commit message [ci skip]')
step.perform!
end
it 'should break the chain' do
expect(step.break?).to be true
end
it 'skips the pipeline' do
expect(pipeline.reload).to be_skipped
end
end
context 'when pipeline has not been skipped' do
before do
step.perform!
end
it 'should not break the chain' do
expect(step.break?).to be false
end
it 'should not skip a pipeline chain' do
expect(pipeline.reload).not_to be_skipped
end
end
context 'when [ci skip] should be ignored' do
let(:command) do
double('command', project: project,
current_user: user,
ignore_skip_ci: true)
end
it 'does not break the chain' do
step.perform!
expect(step.break?).to be false
end
end
context 'when pipeline should be skipped but not persisted' do
let(:command) do
double('command', project: project,
current_user: user,
ignore_skip_ci: false,
save_incompleted: false)
end
before do
allow(pipeline).to receive(:git_commit_message)
.and_return('commit message [ci skip]')
step.perform!
end
it 'breaks the chain' do
expect(step.break?).to be true
end
it 'does not skip pipeline' do
expect(pipeline.reload).not_to be_skipped
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Pipeline::Chain::Validate::Abilities do
set(:project) { create(:project, :repository) }
set(:user) { create(:user) }
let(:pipeline) do
build_stubbed(:ci_pipeline, ref: ref, project: project)
end
let(:command) do
double('command', project: project, current_user: user)
end
let(:step) { described_class.new(pipeline, command) }
let(:ref) { 'master' }
context 'when users has no ability to run a pipeline' do
before do
step.perform!
end
it 'adds an error about insufficient permissions' do
expect(pipeline.errors.to_a)
.to include /Insufficient permissions/
end
it 'breaks the pipeline builder chain' do
expect(step.break?).to eq true
end
end
context 'when user has ability to create a pipeline' do
before do
project.add_developer(user)
step.perform!
end
it 'does not invalidate the pipeline' do
expect(pipeline).to be_valid
end
it 'does not break the chain' do
expect(step.break?).to eq false
end
end
describe '#allowed_to_create?' do
subject { step.allowed_to_create? }
context 'when user is a developer' do
before do
project.add_developer(user)
end
it { is_expected.to be_truthy }
context 'when the branch is protected' do
let!(:protected_branch) do
create(:protected_branch, project: project, name: ref)
end
it { is_expected.to be_falsey }
context 'when developers are allowed to merge' do
let!(:protected_branch) do
create(:protected_branch,
:developers_can_merge,
project: project,
name: ref)
end
it { is_expected.to be_truthy }
end
end
context 'when the tag is protected' do
let(:ref) { 'v1.0.0' }
let!(:protected_tag) do
create(:protected_tag, project: project, name: ref)
end
it { is_expected.to be_falsey }
context 'when developers are allowed to create the tag' do
let!(:protected_tag) do
create(:protected_tag,
:developers_can_create,
project: project,
name: ref)
end
it { is_expected.to be_truthy }
end
end
end
context 'when user is a master' do
before do
project.add_master(user)
end
it { is_expected.to be_truthy }
context 'when the branch is protected' do
let!(:protected_branch) do
create(:protected_branch, project: project, name: ref)
end
it { is_expected.to be_truthy }
end
context 'when the tag is protected' do
let(:ref) { 'v1.0.0' }
let!(:protected_tag) do
create(:protected_tag, project: project, name: ref)
end
it { is_expected.to be_truthy }
context 'when no one can create the tag' do
let!(:protected_tag) do
create(:protected_tag,
:no_one_can_create,
project: project,
name: ref)
end
it { is_expected.to be_falsey }
end
end
end
context 'when owner cannot create pipeline' do
it { is_expected.to be_falsey }
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Pipeline::Chain::Validate::Config do
set(:project) { create(:project) }
set(:user) { create(:user) }
let(:command) do
double('command', project: project,
current_user: user,
save_incompleted: true)
end
let!(:step) { described_class.new(pipeline, command) }
before do
step.perform!
end
context 'when pipeline has no YAML configuration' do
let(:pipeline) do
build_stubbed(:ci_pipeline, project: project)
end
it 'appends errors about missing configuration' do
expect(pipeline.errors.to_a)
.to include 'Missing .gitlab-ci.yml file'
end
it 'breaks the chain' do
expect(step.break?).to be true
end
end
context 'when YAML configuration contains errors' do
let(:pipeline) do
build(:ci_pipeline, project: project, config: 'invalid YAML')
end
it 'appends errors about YAML errors' do
expect(pipeline.errors.to_a)
.to include 'Invalid configuration format'
end
it 'breaks the chain' do
expect(step.break?).to be true
end
context 'when saving incomplete pipeline is allowed' do
let(:command) do
double('command', project: project,
current_user: user,
save_incompleted: true)
end
it 'fails the pipeline' do
expect(pipeline.reload).to be_failed
end
end
context 'when saving incomplete pipeline is not allowed' do
let(:command) do
double('command', project: project,
current_user: user,
save_incompleted: false)
end
it 'does not drop pipeline' do
expect(pipeline).not_to be_failed
expect(pipeline).not_to be_persisted
end
end
end
context 'when pipeline has no stages / jobs' do
let(:config) do
{ rspec: {
script: 'ls',
only: ['something']
} }
end
let(:pipeline) do
build(:ci_pipeline, project: project, config: config)
end
it 'appends an error about missing stages' do
expect(pipeline.errors.to_a)
.to include 'No stages / jobs for this pipeline.'
end
it 'breaks the chain' do
expect(step.break?).to be true
end
end
context 'when pipeline contains configuration validation errors' do
let(:config) { { rspec: {} } }
let(:pipeline) do
build(:ci_pipeline, project: project, config: config)
end
it 'appends configuration validation errors to pipeline errors' do
expect(pipeline.errors.to_a)
.to include "jobs:rspec config can't be blank"
end
it 'breaks the chain' do
expect(step.break?).to be true
end
end
context 'when pipeline is correct and complete' do
let(:pipeline) do
build(:ci_pipeline_with_one_job, project: project)
end
it 'does not invalidate the pipeline' do
expect(pipeline).to be_valid
end
it 'does not break the chain' do
expect(step.break?).to be false
end
end
end
require 'spec_helper'
describe Gitlab::Ci::Pipeline::Chain::Validate::Repository do
set(:project) { create(:project, :repository) }
set(:user) { create(:user) }
let(:command) do
double('command', project: project, current_user: user)
end
let!(:step) { described_class.new(pipeline, command) }
before do
step.perform!
end
context 'when pipeline ref and sha exists' do
let(:pipeline) do
build_stubbed(:ci_pipeline, ref: 'master', sha: '123', project: project)
end
it 'does not break the chain' do
expect(step.break?).to be false
end
it 'does not append pipeline errors' do
expect(pipeline.errors).to be_empty
end
end
context 'when pipeline ref does not exist' do
let(:pipeline) do
build_stubbed(:ci_pipeline, ref: 'something', project: project)
end
it 'breaks the chain' do
expect(step.break?).to be true
end
it 'adds an error about missing ref' do
expect(pipeline.errors.to_a)
.to include 'Reference not found'
end
end
context 'when pipeline does not have SHA set' do
let(:pipeline) do
build_stubbed(:ci_pipeline, ref: 'master', sha: nil, project: project)
end
it 'breaks the chain' do
expect(step.break?).to be true
end
it 'adds an error about missing SHA' do
expect(pipeline.errors.to_a)
.to include 'Commit not found'
end
end
end
require 'spec_helper' require 'spec_helper'
describe Gitlab::Ci::PipelineDuration do describe Gitlab::Ci::Pipeline::Duration do
let(:calculated_duration) { calculate(data) } let(:calculated_duration) { calculate(data) }
shared_examples 'calculating duration' do shared_examples 'calculating duration' do
...@@ -107,9 +107,9 @@ describe Gitlab::Ci::PipelineDuration do ...@@ -107,9 +107,9 @@ describe Gitlab::Ci::PipelineDuration do
def calculate(data) def calculate(data)
periods = data.shuffle.map do |(first, last)| periods = data.shuffle.map do |(first, last)|
Gitlab::Ci::PipelineDuration::Period.new(first, last) described_class::Period.new(first, last)
end end
Gitlab::Ci::PipelineDuration.from_periods(periods.sort_by(&:first)) described_class.from_periods(periods.sort_by(&:first))
end end
end end
...@@ -133,6 +133,26 @@ describe Ci::CreatePipelineService do ...@@ -133,6 +133,26 @@ describe Ci::CreatePipelineService do
expect(merge_request.reload.head_pipeline).to eq head_pipeline expect(merge_request.reload.head_pipeline).to eq head_pipeline
end end
end end
context 'when pipeline has been skipped' do
before do
allow_any_instance_of(Ci::Pipeline)
.to receive(:git_commit_message)
.and_return('some commit [ci skip]')
end
it 'updates merge request head pipeline' do
merge_request = create(:merge_request, source_branch: 'master',
target_branch: 'feature',
source_project: project)
head_pipeline = execute_service
expect(head_pipeline).to be_skipped
expect(head_pipeline).to be_persisted
expect(merge_request.reload.head_pipeline).to eq head_pipeline
end
end
end end
context 'auto-cancel enabled' do context 'auto-cancel enabled' do
...@@ -481,104 +501,4 @@ describe Ci::CreatePipelineService do ...@@ -481,104 +501,4 @@ describe Ci::CreatePipelineService do
end end
end end
end end
describe '#allowed_to_create?' do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:ref) { 'master' }
subject do
described_class.new(project, user, ref: ref)
.send(:allowed_to_create?)
end
context 'when user is a developer' do
before do
project.add_developer(user)
end
it { is_expected.to be_truthy }
context 'when the branch is protected' do
let!(:protected_branch) do
create(:protected_branch, project: project, name: ref)
end
it { is_expected.to be_falsey }
context 'when developers are allowed to merge' do
let!(:protected_branch) do
create(:protected_branch,
:developers_can_merge,
project: project,
name: ref)
end
it { is_expected.to be_truthy }
end
end
context 'when the tag is protected' do
let(:ref) { 'v1.0.0' }
let!(:protected_tag) do
create(:protected_tag, project: project, name: ref)
end
it { is_expected.to be_falsey }
context 'when developers are allowed to create the tag' do
let!(:protected_tag) do
create(:protected_tag,
:developers_can_create,
project: project,
name: ref)
end
it { is_expected.to be_truthy }
end
end
end
context 'when user is a master' do
before do
project.add_master(user)
end
it { is_expected.to be_truthy }
context 'when the branch is protected' do
let!(:protected_branch) do
create(:protected_branch, project: project, name: ref)
end
it { is_expected.to be_truthy }
end
context 'when the tag is protected' do
let(:ref) { 'v1.0.0' }
let!(:protected_tag) do
create(:protected_tag, project: project, name: ref)
end
it { is_expected.to be_truthy }
context 'when no one can create the tag' do
let!(:protected_tag) do
create(:protected_tag,
:no_one_can_create,
project: project,
name: ref)
end
it { is_expected.to be_falsey }
end
end
end
context 'when owner cannot create pipeline' do
it { is_expected.to be_falsey }
end
end
end end
...@@ -70,12 +70,15 @@ describe PostReceive do ...@@ -70,12 +70,15 @@ describe PostReceive do
context "creates a Ci::Pipeline for every change" do context "creates a Ci::Pipeline for every change" do
before do before do
allow_any_instance_of(Ci::CreatePipelineService).to receive(:commit) do
OpenStruct.new(id: '123456')
end
allow_any_instance_of(Ci::CreatePipelineService).to receive(:branch?).and_return(true)
allow_any_instance_of(Repository).to receive(:ref_exists?).and_return(true)
stub_ci_pipeline_to_return_yaml_file stub_ci_pipeline_to_return_yaml_file
# TODO, don't stub private methods
#
allow_any_instance_of(Ci::CreatePipelineService)
.to receive(:commit).and_return(OpenStruct.new(id: '123456'))
allow_any_instance_of(Repository)
.to receive(:branch_exists?).and_return(true)
end end
it { expect { subject }.to change { Ci::Pipeline.count }.by(2) } it { expect { subject }.to change { Ci::Pipeline.count }.by(2) }
......
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