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:
commit
0bba522f74
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
|
|
Loading…
Reference in New Issue