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
This commit is contained in:
Kamil Trzciński 2017-09-27 14:44:28 +00:00
commit 0bba522f74
21 changed files with 1007 additions and 371 deletions

View File

@ -434,7 +434,7 @@ module Ci
def update_duration
return unless started_at
self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
self.duration = Gitlab::Ci::Pipeline::Duration.from_pipeline(self)
end
def execute_hooks

View File

@ -2,110 +2,55 @@ module Ci
class CreatePipelineService < BaseService
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(
source: source,
project: project,
ref: ref,
sha: sha,
before_sha: before_sha,
tag: tag?,
tag: tag_exists?,
trigger_requests: Array(trigger_request),
user: current_user,
pipeline_schedule: schedule,
protected: project.protected_for?(ref)
)
result = validate_project_and_git_items ||
validate_pipeline(ignore_skip_ci: ignore_skip_ci,
save_on_errors: save_on_errors)
command = OpenStruct.new(ignore_skip_ci: ignore_skip_ci,
save_incompleted: 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
Ci::Pipeline.transaction do
pipeline.save!
sequence.build! do |pipeline, sequence|
update_merge_requests_head_pipeline if pipeline.persisted?
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
.new(project, current_user)
.execute(pipeline)
pipeline.process!
end
rescue ActiveRecord::RecordInvalid => e
return error("Failed to persist the pipeline: #{e}")
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
private
def validate_project_and_git_items
unless project.builds_enabled?
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
def commit
@commit ||= project.commit(origin_sha || origin_ref)
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
def allowed_to_create?
return unless can?(current_user, :create_pipeline, project)
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
def sha
commit.try(:id)
end
def update_merge_requests_head_pipeline
@ -115,11 +60,6 @@ module Ci
.update_all(head_pipeline_id: @pipeline.id)
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
Gitlab::OptimisticLocking.retry_lock(auto_cancelable_pipelines) do |cancelables|
cancelables.find_each do |cancelable|
@ -136,14 +76,6 @@ module Ci
.created_or_pending
end
def commit
@commit ||= project.commit(origin_sha || origin_ref)
end
def sha
commit.try(:id)
end
def before_sha
params[:checkout_sha] || params[:before] || Gitlab::Git::BLANK_SHA
end
@ -156,41 +88,17 @@ module Ci
params[:ref]
end
def branch?
return @is_branch if defined?(@is_branch)
@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)
def tag_exists?
project.repository.tag_exists?(ref)
end
def ref
@ref ||= Gitlab::Git.ref_name(origin_ref)
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
@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

View File

@ -0,0 +1,27 @@
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

View File

@ -0,0 +1,29 @@
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

View File

@ -0,0 +1,25 @@
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

View File

@ -0,0 +1,36 @@
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

View File

@ -0,0 +1,33 @@
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

View File

@ -0,0 +1,54 @@
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

View File

@ -0,0 +1,35 @@
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

View File

@ -0,0 +1,30 @@
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

View File

@ -0,0 +1,143 @@
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

View File

@ -1,141 +0,0 @@
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

View File

@ -0,0 +1,66 @@
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

View File

@ -0,0 +1,55 @@
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

View File

@ -0,0 +1,85 @@
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

View File

@ -0,0 +1,142 @@
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

View File

@ -0,0 +1,126 @@
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

View File

@ -0,0 +1,60 @@
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

View File

@ -1,6 +1,6 @@
require 'spec_helper'
describe Gitlab::Ci::PipelineDuration do
describe Gitlab::Ci::Pipeline::Duration do
let(:calculated_duration) { calculate(data) }
shared_examples 'calculating duration' do
@ -107,9 +107,9 @@ describe Gitlab::Ci::PipelineDuration do
def calculate(data)
periods = data.shuffle.map do |(first, last)|
Gitlab::Ci::PipelineDuration::Period.new(first, last)
described_class::Period.new(first, last)
end
Gitlab::Ci::PipelineDuration.from_periods(periods.sort_by(&:first))
described_class.from_periods(periods.sort_by(&:first))
end
end

View File

@ -133,6 +133,26 @@ describe Ci::CreatePipelineService do
expect(merge_request.reload.head_pipeline).to eq head_pipeline
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
context 'auto-cancel enabled' do
@ -481,104 +501,4 @@ describe Ci::CreatePipelineService do
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

View File

@ -70,12 +70,15 @@ describe PostReceive do
context "creates a Ci::Pipeline for every change" 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
# 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
it { expect { subject }.to change { Ci::Pipeline.count }.by(2) }