Commit c66d25b9 authored by Kamil Trzciński's avatar Kamil Trzciński

Merge branch 'backstage/gb/populating-pipeline-refactoring' into 'master'

Improve how we populate pipeline with stages and builds

Closes #43941

See merge request gitlab-org/gitlab-ce!17841
parents 3adbc579 5b019f43
...@@ -6,6 +6,7 @@ module Ci ...@@ -6,6 +6,7 @@ module Ci
include AfterCommitQueue include AfterCommitQueue
include Presentable include Presentable
include Gitlab::OptimisticLocking include Gitlab::OptimisticLocking
include Gitlab::Utils::StrongMemoize
belongs_to :project, inverse_of: :pipelines belongs_to :project, inverse_of: :pipelines
belongs_to :user belongs_to :user
...@@ -361,21 +362,23 @@ module Ci ...@@ -361,21 +362,23 @@ module Ci
def stage_seeds def stage_seeds
return [] unless config_processor return [] unless config_processor
@stage_seeds ||= config_processor.stage_seeds(self) strong_memoize(:stage_seeds) do
seeds = config_processor.stages_attributes.map do |attributes|
Gitlab::Ci::Pipeline::Seed::Stage.new(self, attributes)
end
seeds.select(&:included?)
end
end end
def seeds_size def seeds_size
@seeds_size ||= stage_seeds.sum(&:size) stage_seeds.sum(&:size)
end end
def has_kubernetes_active? def has_kubernetes_active?
project.deployment_platform&.active? project.deployment_platform&.active?
end end
def has_stage_seeds?
stage_seeds.any?
end
def has_warnings? def has_warnings?
builds.latest.failed_but_allowed.any? builds.latest.failed_but_allowed.any?
end end
...@@ -388,6 +391,9 @@ module Ci ...@@ -388,6 +391,9 @@ module Ci
end end
end end
##
# TODO, setting yaml_errors should be moved to the pipeline creation chain.
#
def config_processor def config_processor
return unless ci_yaml_file return unless ci_yaml_file
return @config_processor if defined?(@config_processor) return @config_processor if defined?(@config_processor)
...@@ -472,6 +478,14 @@ module Ci ...@@ -472,6 +478,14 @@ module Ci
end end
end end
def protected_ref?
strong_memoize(:protected_ref) { project.protected_for?(ref) }
end
def legacy_trigger
strong_memoize(:legacy_trigger) { trigger_requests.first }
end
def predefined_variables def predefined_variables
Gitlab::Ci::Variables::Collection.new Gitlab::Ci::Variables::Collection.new
.append(key: 'CI_PIPELINE_ID', value: id.to_s) .append(key: 'CI_PIPELINE_ID', value: id.to_s)
......
...@@ -7,6 +7,7 @@ module Ci ...@@ -7,6 +7,7 @@ module Ci
Gitlab::Ci::Pipeline::Chain::Validate::Repository, Gitlab::Ci::Pipeline::Chain::Validate::Repository,
Gitlab::Ci::Pipeline::Chain::Validate::Config, Gitlab::Ci::Pipeline::Chain::Validate::Config,
Gitlab::Ci::Pipeline::Chain::Skip, Gitlab::Ci::Pipeline::Chain::Skip,
Gitlab::Ci::Pipeline::Chain::Populate,
Gitlab::Ci::Pipeline::Chain::Create].freeze Gitlab::Ci::Pipeline::Chain::Create].freeze
def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, &block) def execute(source, ignore_skip_ci: false, save_on_errors: true, trigger_request: nil, schedule: nil, &block)
......
module Ci
class CreatePipelineStagesService < BaseService
def execute(pipeline)
pipeline.stage_seeds.each do |seed|
seed.user = current_user
seed.create! do |build|
##
# Create the environment before the build starts. This sets its slug and
# makes it available as an environment variable
#
if build.has_environment?
environment_name = build.expanded_environment_name
project.environments.find_or_create_by(name: environment_name)
end
end
end
end
end
end
...@@ -16,8 +16,8 @@ module Ci ...@@ -16,8 +16,8 @@ module Ci
pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: params[:ref]) pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: params[:ref])
.execute(:trigger, ignore_skip_ci: true) do |pipeline| .execute(:trigger, ignore_skip_ci: true) do |pipeline|
pipeline.trigger_requests.create!(trigger: trigger) pipeline.trigger_requests.build(trigger: trigger)
create_pipeline_variables!(pipeline) pipeline.variables.build(variables)
end end
if pipeline.persisted? if pipeline.persisted?
...@@ -33,14 +33,10 @@ module Ci ...@@ -33,14 +33,10 @@ module Ci
end end
end end
def create_pipeline_variables!(pipeline) def variables
return unless params[:variables] params[:variables].to_h.map do |key, value|
variables = params[:variables].map do |key, value|
{ key: key, value: value } { key: key, value: value }
end end
pipeline.variables.create!(variables)
end end
end end
end end
...@@ -9,11 +9,16 @@ module Gitlab ...@@ -9,11 +9,16 @@ module Gitlab
::Ci::Pipeline.transaction do ::Ci::Pipeline.transaction do
pipeline.save! pipeline.save!
@command.seeds_block&.call(pipeline) ##
# Create environments before the pipeline starts.
::Ci::CreatePipelineStagesService #
.new(project, current_user) pipeline.builds.each do |build|
.execute(pipeline) if build.has_environment?
project.environments.find_or_create_by(
name: build.expanded_environment_name
)
end
end
end end
rescue ActiveRecord::RecordInvalid => e rescue ActiveRecord::RecordInvalid => e
error("Failed to persist the pipeline: #{e}") error("Failed to persist the pipeline: #{e}")
......
module Gitlab
module Ci
module Pipeline
module Chain
class Populate < Chain::Base
include Chain::Helpers
PopulateError = Class.new(StandardError)
def perform!
##
# Populate pipeline with block argument of CreatePipelineService#execute.
#
@command.seeds_block&.call(pipeline)
##
# Populate pipeline with all stages and builds from pipeline seeds.
#
pipeline.stage_seeds.each do |stage|
stage.user = current_user
pipeline.stages << stage.to_resource
stage.seeds.each do |build|
pipeline.builds << build.to_resource
end
end
if pipeline.stages.none?
return error('No stages / jobs for this pipeline.')
end
if pipeline.invalid?
return error('Failed to build the pipeline!')
end
raise Populate::PopulateError if pipeline.persisted?
end
def break?
pipeline.errors.any?
end
end
end
end
end
end
...@@ -16,11 +16,7 @@ module Gitlab ...@@ -16,11 +16,7 @@ module Gitlab
@pipeline.drop!(:config_error) @pipeline.drop!(:config_error)
end end
return error(@pipeline.yaml_errors) error(@pipeline.yaml_errors)
end
unless @pipeline.has_stage_seeds?
return error('No stages / jobs for this pipeline.')
end end
end end
......
module Gitlab
module Ci
module Pipeline
module Seed
class Base
def attributes
raise NotImplementedError
end
def included?
raise NotImplementedError
end
def to_resource
raise NotImplementedError
end
end
end
end
end
end
module Gitlab
module Ci
module Pipeline
module Seed
class Build < Seed::Base
include Gitlab::Utils::StrongMemoize
delegate :dig, to: :@attributes
def initialize(pipeline, attributes)
@pipeline = pipeline
@attributes = attributes
@only = attributes.delete(:only)
@except = attributes.delete(:except)
end
def user=(current_user)
@attributes.merge!(user: current_user)
end
def included?
strong_memoize(:inclusion) do
only_specs = Gitlab::Ci::Build::Policy.fabricate(@only)
except_specs = Gitlab::Ci::Build::Policy.fabricate(@except)
only_specs.all? { |spec| spec.satisfied_by?(@pipeline) } &&
except_specs.none? { |spec| spec.satisfied_by?(@pipeline) }
end
end
def attributes
@attributes.merge(
pipeline: @pipeline,
project: @pipeline.project,
ref: @pipeline.ref,
tag: @pipeline.tag,
trigger_request: @pipeline.legacy_trigger,
protected: @pipeline.protected_ref?
)
end
def to_resource
strong_memoize(:resource) do
::Ci::Build.new(attributes)
end
end
end
end
end
end
end
module Gitlab
module Ci
module Pipeline
module Seed
class Stage < Seed::Base
include Gitlab::Utils::StrongMemoize
delegate :size, to: :seeds
delegate :dig, to: :seeds
def initialize(pipeline, attributes)
@pipeline = pipeline
@attributes = attributes
@builds = attributes.fetch(:builds).map do |attributes|
Seed::Build.new(@pipeline, attributes)
end
end
def user=(current_user)
@builds.each { |seed| seed.user = current_user }
end
def attributes
{ name: @attributes.fetch(:name),
pipeline: @pipeline,
project: @pipeline.project }
end
def seeds
strong_memoize(:seeds) do
@builds.select(&:included?)
end
end
def included?
seeds.any?
end
def to_resource
strong_memoize(:stage) do
::Ci::Stage.new(attributes).tap do |stage|
seeds.each { |seed| stage.builds << seed.to_resource }
end
end
end
end
end
end
end
end
module Gitlab
module Ci
module Stage
class Seed
include ::Gitlab::Utils::StrongMemoize
attr_reader :pipeline
delegate :project, to: :pipeline
delegate :size, to: :@jobs
def initialize(pipeline, stage, jobs)
@pipeline = pipeline
@stage = { name: stage }
@jobs = jobs.to_a.dup
end
def user=(current_user)
@jobs.map! do |attributes|
attributes.merge(user: current_user)
end
end
def stage
@stage.merge(project: project)
end
def builds
trigger = pipeline.trigger_requests.first
@jobs.map do |attributes|
attributes.merge(project: project,
ref: pipeline.ref,
tag: pipeline.tag,
trigger_request: trigger,
protected: protected_ref?)
end
end
def create!
pipeline.stages.create!(stage).tap do |stage|
builds_attributes = builds.map do |attributes|
attributes.merge(stage_id: stage.id)
end
pipeline.builds.create!(builds_attributes).each do |build|
yield build if block_given?
end
end
end
private
def protected_ref?
strong_memoize(:protected_ref) do
project.protected_for?(pipeline.ref)
end
end
end
end
end
end
...@@ -27,7 +27,7 @@ module Gitlab ...@@ -27,7 +27,7 @@ module Gitlab
end end
def build_attributes(name) def build_attributes(name)
job = @jobs[name.to_sym] || {} job = @jobs.fetch(name.to_sym, {})
{ stage_idx: @stages.index(job[:stage]), { stage_idx: @stages.index(job[:stage]),
stage: job[:stage], stage: job[:stage],
...@@ -53,30 +53,24 @@ module Gitlab ...@@ -53,30 +53,24 @@ module Gitlab
}.compact } }.compact }
end end
def pipeline_stage_builds(stage, pipeline) def stage_builds_attributes(stage)
selected_jobs = @jobs.select do |_, job| @jobs.values
next unless job[:stage] == stage .select { |job| job[:stage] == stage }
.map { |job| build_attributes(job[:name]) }
only_specs = Gitlab::Ci::Build::Policy
.fabricate(job.fetch(:only, {}))
except_specs = Gitlab::Ci::Build::Policy
.fabricate(job.fetch(:except, {}))
only_specs.all? { |spec| spec.satisfied_by?(pipeline) } &&
except_specs.none? { |spec| spec.satisfied_by?(pipeline) }
end
selected_jobs.map { |_, job| build_attributes(job[:name]) }
end end
def stage_seeds(pipeline) def stages_attributes
seeds = @stages.uniq.map do |stage| @stages.uniq.map do |stage|
builds = pipeline_stage_builds(stage, pipeline) seeds = stage_builds_attributes(stage).map do |attributes|
job = @jobs.fetch(attributes[:name].to_sym)
Gitlab::Ci::Stage::Seed.new(pipeline, stage, builds) if builds.any? attributes
end .merge(only: job.fetch(:only, {}))
.merge(except: job.fetch(:except, {}))
end
seeds.compact { name: stage, index: @stages.index(stage), builds: seeds }
end
end end
def self.validation_message(content) def self.validation_message(content)
......
...@@ -5,23 +5,23 @@ describe Gitlab::Ci::Pipeline::Chain::Create do ...@@ -5,23 +5,23 @@ describe Gitlab::Ci::Pipeline::Chain::Create do
set(:user) { create(:user) } set(:user) { create(:user) }
let(:pipeline) do let(:pipeline) do
build(:ci_pipeline_with_one_job, project: project, build(:ci_empty_pipeline, project: project, ref: 'master')
ref: 'master')
end end
let(:command) do let(:command) do
Gitlab::Ci::Pipeline::Chain::Command.new( Gitlab::Ci::Pipeline::Chain::Command.new(
project: project, project: project, current_user: user)
current_user: user, seeds_block: nil)
end end
let(:step) { described_class.new(pipeline, command) } let(:step) { described_class.new(pipeline, command) }
before do
step.perform!
end
context 'when pipeline is ready to be saved' do context 'when pipeline is ready to be saved' do
before do
pipeline.stages.build(name: 'test', project: project)
step.perform!
end
it 'saves a pipeline' do it 'saves a pipeline' do
expect(pipeline).to be_persisted expect(pipeline).to be_persisted
end end
...@@ -32,6 +32,7 @@ describe Gitlab::Ci::Pipeline::Chain::Create do ...@@ -32,6 +32,7 @@ describe Gitlab::Ci::Pipeline::Chain::Create do
it 'creates stages' do it 'creates stages' do
expect(pipeline.reload.stages).to be_one expect(pipeline.reload.stages).to be_one
expect(pipeline.stages.first).to be_persisted
end end
end end
...@@ -40,6 +41,10 @@ describe Gitlab::Ci::Pipeline::Chain::Create do ...@@ -40,6 +41,10 @@ describe Gitlab::Ci::Pipeline::Chain::Create do
build(:ci_pipeline, project: project, ref: nil) build(:ci_pipeline, project: project, ref: nil)
end end
before do
step.perform!
end
it 'breaks the chain' do it 'breaks the chain' do
expect(step.break?).to be true expect(step.break?).to be true
end end
...@@ -49,18 +54,4 @@ describe Gitlab::Ci::Pipeline::Chain::Create do ...@@ -49,18 +54,4 @@ describe Gitlab::Ci::Pipeline::Chain::Create do
.to include /Failed to persist the pipeline/ .to include /Failed to persist the pipeline/
end end
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 end
require 'spec_helper'
describe Gitlab::Ci::Pipeline::Chain::Populate 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
Gitlab::Ci::Pipeline::Chain::Command.new(
project: project,
current_user: user,
seeds_block: nil)
end
let(:step) { described_class.new(pipeline, command) }
context 'when pipeline doesn not have seeds block' do
before do
step.perform!
end
it 'does not persist the pipeline' do
expect(pipeline).not_to be_persisted
end
it 'does not break the chain' do
expect(step.break?).to be false
end
it 'populates pipeline with stages' do
expect(pipeline.stages).to be_one
expect(pipeline.stages.first).not_to be_persisted
end
it 'populates pipeline with builds' do
expect(pipeline.builds).to be_one
expect(pipeline.builds.first).not_to be_persisted
expect(pipeline.stages.first.builds).to be_one
expect(pipeline.stages.first.builds.first).not_to be_persisted
end
end
context 'when pipeline is empty' do
let(:config) do
{ rspec: {
script: 'ls',
only: ['something']
} }
end
let(:pipeline) do
build(:ci_pipeline, project: project, config: config)
end
before do
step.perform!
end
it 'breaks the chain' do
expect(step.break?).to be true
end
it 'appends an error about missing stages' do
expect(pipeline.errors.to_a)
.to include 'No stages / jobs for this pipeline.'
end
end
context 'when pipeline has validation errors' do
let(:pipeline) do
build(:ci_pipeline, project: project, ref: nil)
end
before do
step.perform!
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 build the pipeline!'
end
end
context 'when there is a seed blocks present' do
let(:command) do
Gitlab::Ci::Pipeline::Chain::Command.new(
project: project,
current_user: user,
seeds_block: seeds_block)
end
context 'when seeds block builds some resources' do
let(:seeds_block) do
->(pipeline) { pipeline.variables.build(key: 'VAR', value: '123') }
end
it 'populates pipeline with resources described in the seeds block' do
step.perform!
expect(pipeline).not_to be_persisted
expect(pipeline.variables).not_to be_empty
expect(pipeline.variables.first).not_to be_persisted
expect(pipeline.variables.first.key).to eq 'VAR'
expect(pipeline.variables.first.value).to eq '123'
end
end
context 'when seeds block tries to persist some resources' do
let(:seeds_block) do
->(pipeline) { pipeline.variables.create!(key: 'VAR', value: '123') }
end
it 'raises exception' do
expect { step.perform! }.to raise_error(ActiveRecord::RecordNotSaved)
end
end
end
context 'when pipeline gets persisted during the process' do
let(:pipeline) { create(:ci_pipeline, project: project) }
it 'raises error' do
expect { step.perform! }.to raise_error(described_class::PopulateError)
end
end
context 'when using only/except build policies' do
let(:config) do
{ rspec: { script: 'rspec', stage: 'test', only: ['master'] },
prod: { script: 'cap prod', stage: 'deploy', only: ['tags'] } }
end
let(:pipeline) do
build(:ci_pipeline, ref: 'master', config: config)
end
it 'populates pipeline according to used policies' do
step.perform!
expect(pipeline.stages.size).to eq 1
expect(pipeline.builds.size).to eq 1
expect(pipeline.builds.first.name).to eq 'rspec'
end
end
end
...@@ -76,28 +76,6 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Config do ...@@ -76,28 +76,6 @@ describe Gitlab::Ci::Pipeline::Chain::Validate::Config do
end 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 context 'when pipeline contains configuration validation errors' do
let(:config) { { rspec: {} } } let(:config) { { rspec: {} } }
......
require 'spec_helper'
describe Gitlab::Ci::Pipeline::Seed::Build do
let(:pipeline) { create(:ci_empty_pipeline) }
let(:attributes) do
{ name: 'rspec',
ref: 'master',
commands: 'rspec' }
end
subject do
described_class.new(pipeline, attributes)
end
describe '#attributes' do
it 'returns hash attributes of a build' do
expect(subject.attributes).to be_a Hash
expect(subject.attributes)
.to include(:name, :project, :ref, :commands)
end
end
describe '#user=' do
let(:user) { build(:user) }
it 'assignes user to a build' do
subject.user = user
expect(subject.attributes).to include(user: user)
end
end
describe '#to_resource' do
it 'returns a valid build resource' do
expect(subject.to_resource).to be_a(::Ci::Build)
expect(subject.to_resource).to be_valid
end
it 'memoizes a resource object' do
build = subject.to_resource
expect(build.object_id).to eq subject.to_resource.object_id
end
it 'can not be persisted without explicit assignment' do
build = subject.to_resource
pipeline.save!
expect(build).not_to be_persisted
end
end
describe 'applying only/except policies' do
context 'when no branch policy is specified' do
let(:attributes) { { name: 'rspec' } }
it { is_expected.to be_included }
end
context 'when branch policy does not match' do
context 'when using only' do
let(:attributes) { { name: 'rspec', only: { refs: ['deploy'] } } }
it { is_expected.not_to be_included }
end
context 'when using except' do
let(:attributes) { { name: 'rspec', except: { refs: ['deploy'] } } }
it { is_expected.to be_included }
end
end
context 'when branch regexp policy does not match' do
context 'when using only' do
let(:attributes) { { name: 'rspec', only: { refs: ['/^deploy$/'] } } }
it { is_expected.not_to be_included }
end
context 'when using except' do
let(:attributes) { { name: 'rspec', except: { refs: ['/^deploy$/'] } } }
it { is_expected.to be_included }
end
end
context 'when branch policy matches' do
context 'when using only' do
let(:attributes) { { name: 'rspec', only: { refs: %w[deploy master] } } }
it { is_expected.to be_included }
end
context 'when using except' do
let(:attributes) { { name: 'rspec', except: { refs: %w[deploy master] } } }
it { is_expected.not_to be_included }
end
end
context 'when keyword policy matches' do
context 'when using only' do
let(:attributes) { { name: 'rspec', only: { refs: ['branches'] } } }
it { is_expected.to be_included }
end
context 'when using except' do
let(:attributes) { { name: 'rspec', except: { refs: ['branches'] } } }
it { is_expected.not_to be_included }
end
end
context 'when keyword policy does not match' do
context 'when using only' do
let(:attributes) { { name: 'rspec', only: { refs: ['tags'] } } }
it { is_expected.not_to be_included }
end
context 'when using except' do
let(:attributes) { { name: 'rspec', except: { refs: ['tags'] } } }
it { is_expected.to be_included }
end
end
context 'when keywords and pipeline source policy matches' do
possibilities = [%w[pushes push],
%w[web web],
%w[triggers trigger],
%w[schedules schedule],
%w[api api],
%w[external external]]
context 'when using only' do
possibilities.each do |keyword, source|
context "when using keyword `#{keyword}` and source `#{source}`" do
let(:pipeline) do
build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source)
end
let(:attributes) { { name: 'rspec', only: { refs: [keyword] } } }
it { is_expected.to be_included }
end
end
end
context 'when using except' do
possibilities.each do |keyword, source|
context "when using keyword `#{keyword}` and source `#{source}`" do
let(:pipeline) do
build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source)
end
let(:attributes) { { name: 'rspec', except: { refs: [keyword] } } }
it { is_expected.not_to be_included }
end
end
end
end
context 'when keywords and pipeline source does not match' do
possibilities = [%w[pushes web],
%w[web push],
%w[triggers schedule],
%w[schedules external],
%w[api trigger],
%w[external api]]
context 'when using only' do
possibilities.each do |keyword, source|
context "when using keyword `#{keyword}` and source `#{source}`" do
let(:pipeline) do
build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source)
end
let(:attributes) { { name: 'rspec', only: { refs: [keyword] } } }
it { is_expected.not_to be_included }
end
end
end
context 'when using except' do
possibilities.each do |keyword, source|
context "when using keyword `#{keyword}` and source `#{source}`" do
let(:pipeline) do
build(:ci_empty_pipeline, ref: 'deploy', tag: false, source: source)
end
let(:attributes) { { name: 'rspec', except: { refs: [keyword] } } }
it { is_expected.to be_included }
end
end
end
end
context 'when repository path matches' do
context 'when using only' do
let(:attributes) do
{ name: 'rspec', only: { refs: ["branches@#{pipeline.project_full_path}"] } }
end
it { is_expected.to be_included }
end
context 'when using except' do
let(:attributes) do
{ name: 'rspec', except: { refs: ["branches@#{pipeline.project_full_path}"] } }
end
it { is_expected.not_to be_included }
end
end
context 'when repository path does not matches' do
context 'when using only' do
let(:attributes) do
{ name: 'rspec', only: { refs: ['branches@fork'] } }
end
it { is_expected.not_to be_included }
end
context 'when using except' do
let(:attributes) do
{ name: 'rspec', except: { refs: ['branches@fork'] } }
end
it { is_expected.to be_included }
end
end
end
end
require 'spec_helper' require 'spec_helper'
describe Gitlab::Ci::Stage::Seed do describe Gitlab::Ci::Pipeline::Seed::Stage do
let(:pipeline) { create(:ci_empty_pipeline) } let(:pipeline) { create(:ci_empty_pipeline) }
let(:builds) do let(:attributes) do
[{ name: 'rspec' }, { name: 'spinach' }] { name: 'test',
index: 0,
builds: [{ name: 'rspec' },
{ name: 'spinach' },
{ name: 'deploy', only: { refs: ['feature'] } }] }
end end
subject do subject do
described_class.new(pipeline, 'test', builds) described_class.new(pipeline, attributes)
end end
describe '#size' do describe '#size' do
...@@ -17,20 +21,46 @@ describe Gitlab::Ci::Stage::Seed do ...@@ -17,20 +21,46 @@ describe Gitlab::Ci::Stage::Seed do
end end
end end
describe '#stage' do describe '#attributes' do
it 'returns hash attributes of a stage' do it 'returns hash attributes of a stage' do
expect(subject.stage).to be_a Hash expect(subject.attributes).to be_a Hash
expect(subject.stage).to include(:name, :project) expect(subject.attributes).to include(:name, :project)
end end
end end
describe '#builds' do describe '#included?' do
it 'returns hash attributes of all builds' do context 'when it contains builds seeds' do
expect(subject.builds.size).to eq 2 let(:attributes) do
expect(subject.builds).to all(include(ref: 'master')) { name: 'test',
expect(subject.builds).to all(include(tag: false)) index: 0,
expect(subject.builds).to all(include(project: pipeline.project)) builds: [{ name: 'deploy', only: { refs: ['master'] } }] }
expect(subject.builds) end
it { is_expected.to be_included }
end
context 'when it does not contain build seeds' do
let(:attributes) do
{ name: 'test',
index: 0,
builds: [{ name: 'deploy', only: { refs: ['feature'] } }] }
end
it { is_expected.not_to be_included }
end
end
describe '#seeds' do
it 'returns build seeds' do
expect(subject.seeds).to all(be_a Gitlab::Ci::Pipeline::Seed::Build)
end
it 'returns build seeds including valid attributes' do
expect(subject.seeds.size).to eq 2
expect(subject.seeds.map(&:attributes)).to all(include(ref: 'master'))
expect(subject.seeds.map(&:attributes)).to all(include(tag: false))
expect(subject.seeds.map(&:attributes)).to all(include(project: pipeline.project))
expect(subject.seeds.map(&:attributes))
.to all(include(trigger_request: pipeline.trigger_requests.first)) .to all(include(trigger_request: pipeline.trigger_requests.first))
end end
...@@ -40,17 +70,27 @@ describe Gitlab::Ci::Stage::Seed do ...@@ -40,17 +70,27 @@ describe Gitlab::Ci::Stage::Seed do
end end
it 'returns protected builds' do it 'returns protected builds' do
expect(subject.builds).to all(include(protected: true)) expect(subject.seeds.map(&:attributes)).to all(include(protected: true))
end end
end end
context 'when a ref is unprotected' do context 'when a ref is not protected' do
before do before do
allow_any_instance_of(Project).to receive(:protected_for?).and_return(false) allow_any_instance_of(Project).to receive(:protected_for?).and_return(false)
end end
it 'returns unprotected builds' do it 'returns unprotected builds' do
expect(subject.builds).to all(include(protected: false)) expect(subject.seeds.map(&:attributes)).to all(include(protected: false))
end
end
it 'filters seeds using only/except policies' do
expect(subject.seeds.map(&:attributes)).to satisfy do |seeds|
seeds.any? { |hash| hash.fetch(:name) == 'rspec' }
end
expect(subject.seeds.map(&:attributes)).not_to satisfy do |seeds|
seeds.any? { |hash| hash.fetch(:name) == 'deploy' }
end end
end end
end end
...@@ -61,13 +101,13 @@ describe Gitlab::Ci::Stage::Seed do ...@@ -61,13 +101,13 @@ describe Gitlab::Ci::Stage::Seed do
it 'assignes relevant pipeline attributes' do it 'assignes relevant pipeline attributes' do
subject.user = user subject.user = user
expect(subject.builds).to all(include(user: user)) expect(subject.seeds.map(&:attributes)).to all(include(user: user))
end end
end end
describe '#create!' do describe '#to_resource' do
it 'creates all stages and builds' do it 'builds a valid stage object with all builds' do
subject.create! subject.to_resource.save!
expect(pipeline.reload.stages.count).to eq 1 expect(pipeline.reload.stages.count).to eq 1
expect(pipeline.reload.builds.count).to eq 2 expect(pipeline.reload.builds.count).to eq 2
...@@ -79,5 +119,15 @@ describe Gitlab::Ci::Stage::Seed do ...@@ -79,5 +119,15 @@ describe Gitlab::Ci::Stage::Seed do
expect(pipeline.stages) expect(pipeline.stages)
.to all(satisfy { |stage| stage.project.present? }) .to all(satisfy { |stage| stage.project.present? })
end end
it 'can not be persisted without explicit pipeline assignment' do
stage = subject.to_resource
pipeline.save!
expect(stage).not_to be_persisted
expect(pipeline.reload.stages.count).to eq 0
expect(pipeline.reload.builds.count).to eq 0
end
end end
end end
...@@ -18,6 +18,34 @@ module Gitlab ...@@ -18,6 +18,34 @@ module Gitlab
describe '#build_attributes' do describe '#build_attributes' do
subject { described_class.new(config).build_attributes(:rspec) } subject { described_class.new(config).build_attributes(:rspec) }
describe 'attributes list' do
let(:config) do
YAML.dump(
before_script: ['pwd'],
rspec: { script: 'rspec' }
)
end
it 'returns valid build attributes' do
expect(subject).to eq({
stage: "test",
stage_idx: 1,
name: "rspec",
commands: "pwd\nrspec",
coverage_regex: nil,
tag_list: [],
options: {
before_script: ["pwd"],
script: ["rspec"]
},
allow_failure: false,
when: "on_success",
environment: nil,
yaml_variables: []
})
end
end
describe 'coverage entry' do describe 'coverage entry' do
describe 'code coverage regexp' do describe 'code coverage regexp' do
let(:config) do let(:config) do
...@@ -105,512 +133,118 @@ module Gitlab ...@@ -105,512 +133,118 @@ module Gitlab
end end
end end
describe '#stage_seeds' do describe '#stages_attributes' do
context 'when no refs policy is specified' do let(:config) do
let(:config) do YAML.dump(
YAML.dump(production: { stage: 'deploy', script: 'cap prod' }, rspec: { script: 'rspec', stage: 'test', only: ['branches'] },
rspec: { stage: 'test', script: 'rspec' }, prod: { script: 'cap prod', stage: 'deploy', only: ['tags'] }
spinach: { stage: 'test', script: 'spinach' }) )
end
let(:pipeline) { create(:ci_empty_pipeline) }
it 'correctly fabricates a stage seeds object' do
seeds = subject.stage_seeds(pipeline)
expect(seeds.size).to eq 2
expect(seeds.first.stage[:name]).to eq 'test'
expect(seeds.second.stage[:name]).to eq 'deploy'
expect(seeds.first.builds.dig(0, :name)).to eq 'rspec'
expect(seeds.first.builds.dig(1, :name)).to eq 'spinach'
expect(seeds.second.builds.dig(0, :name)).to eq 'production'
end
end
context 'when refs policy is specified' do
let(:config) do
YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['master'] },
spinach: { stage: 'test', script: 'spinach', only: ['tags'] })
end
let(:pipeline) do
create(:ci_empty_pipeline, ref: 'feature', tag: true)
end
it 'returns stage seeds only assigned to master to master' do
seeds = subject.stage_seeds(pipeline)
expect(seeds.size).to eq 1
expect(seeds.first.stage[:name]).to eq 'test'
expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
end
end
context 'when source policy is specified' do
let(:config) do
YAML.dump(production: { stage: 'deploy', script: 'cap prod', only: ['triggers'] },
spinach: { stage: 'test', script: 'spinach', only: ['schedules'] })
end
let(:pipeline) do
create(:ci_empty_pipeline, source: :schedule)
end
it 'returns stage seeds only assigned to schedules' do
seeds = subject.stage_seeds(pipeline)
expect(seeds.size).to eq 1
expect(seeds.first.stage[:name]).to eq 'test'
expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
end
end end
context 'when kubernetes policy is specified' do let(:attributes) do
let(:config) do [{ name: "build",
YAML.dump( index: 0,
spinach: { stage: 'test', script: 'spinach' }, builds: [] },
production: { { name: "test",
stage: 'deploy', index: 1,
script: 'cap', builds:
only: { kubernetes: 'active' } [{ stage_idx: 1,
} stage: "test",
) commands: "rspec",
end tag_list: [],
name: "rspec",
context 'when kubernetes is active' do allow_failure: false,
shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do when: "on_success",
it 'returns seeds for kubernetes dependent job' do environment: nil,
seeds = subject.stage_seeds(pipeline) coverage_regex: nil,
yaml_variables: [],
expect(seeds.size).to eq 2 options: { script: ["rspec"] },
expect(seeds.first.builds.dig(0, :name)).to eq 'spinach' only: { refs: ["branches"] },
expect(seeds.second.builds.dig(0, :name)).to eq 'production' except: {} }] },
end { name: "deploy",
end index: 2,
builds:
context 'when user configured kubernetes from Integration > Kubernetes' do [{ stage_idx: 2,
let(:project) { create(:kubernetes_project) } stage: "deploy",
let(:pipeline) { create(:ci_empty_pipeline, project: project) } commands: "cap prod",
tag_list: [],
it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes' name: "prod",
end allow_failure: false,
when: "on_success",
context 'when user configured kubernetes from CI/CD > Clusters' do environment: nil,
let!(:cluster) { create(:cluster, :project, :provided_by_gcp) } coverage_regex: nil,
let(:project) { cluster.project } yaml_variables: [],
let(:pipeline) { create(:ci_empty_pipeline, project: project) } options: { script: ["cap prod"] },
only: { refs: ["tags"] },
it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes' except: {} }] }]
end end
end
it 'returns stages seed attributes' do
context 'when kubernetes is not active' do expect(subject.stages_attributes).to eq attributes
it 'does not return seeds for kubernetes dependent job' do
seeds = subject.stage_seeds(pipeline)
expect(seeds.size).to eq 1
expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
end
end
end end
end end
describe "#pipeline_stage_builds" do describe 'only / except policies validations' do
let(:type) { 'test' } context 'when `only` has an invalid value' do
let(:config) { { rspec: { script: "rspec", type: "test", only: only } } }
let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
it "returns builds if no branch specified" do context 'when it is integer' do
config = YAML.dump({ let(:only) { 1 }
before_script: ["pwd"],
rspec: { script: "rspec" }
})
config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(1)
expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).first).to eq({
stage: "test",
stage_idx: 1,
name: "rspec",
commands: "pwd\nrspec",
coverage_regex: nil,
tag_list: [],
options: {
before_script: ["pwd"],
script: ["rspec"]
},
allow_failure: false,
when: "on_success",
environment: nil,
yaml_variables: []
})
end
describe 'only' do
it "does not return builds if only has another branch" do
config = YAML.dump({
before_script: ["pwd"],
rspec: { script: "rspec", only: ["deploy"] }
})
config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(0)
end
it "does not return builds if only has regexp with another branch" do
config = YAML.dump({
before_script: ["pwd"],
rspec: { script: "rspec", only: ["/^deploy$/"] }
})
config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(0)
end
it "returns builds if only has specified this branch" do
config = YAML.dump({
before_script: ["pwd"],
rspec: { script: "rspec", only: ["master"] }
})
config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(1)
end
it "returns builds if only has a list of branches including specified" do
config = YAML.dump({
before_script: ["pwd"],
rspec: { script: "rspec", type: type, only: %w(master deploy) }
})
config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(1)
end
it "returns builds if only has a branches keyword specified" do
config = YAML.dump({
before_script: ["pwd"],
rspec: { script: "rspec", type: type, only: ["branches"] }
})
config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(1)
end
it "does not return builds if only has a tags keyword" do
config = YAML.dump({
before_script: ["pwd"],
rspec: { script: "rspec", type: type, only: ["tags"] }
})
config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(0)
end
it "returns builds if only has special keywords specified and source matches" do it do
possibilities = [{ keyword: 'pushes', source: 'push' }, expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
{ keyword: 'web', source: 'web' }, 'jobs:rspec:only has to be either an array of conditions or a hash')
{ keyword: 'triggers', source: 'trigger' },
{ keyword: 'schedules', source: 'schedule' },
{ keyword: 'api', source: 'api' },
{ keyword: 'external', source: 'external' }]
possibilities.each do |possibility|
config = YAML.dump({
before_script: ["pwd"],
rspec: { script: "rspec", type: type, only: [possibility[:keyword]] }
})
config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.pipeline_stage_builds(type, pipeline(ref: 'deploy', tag: false, source: possibility[:source])).size).to eq(1)
end end
end end
it "does not return builds if only has special keywords specified and source doesn't match" do context 'when it is an array of integers' do
possibilities = [{ keyword: 'pushes', source: 'web' }, let(:only) { [1, 1] }
{ keyword: 'web', source: 'push' },
{ keyword: 'triggers', source: 'schedule' },
{ keyword: 'schedules', source: 'external' },
{ keyword: 'api', source: 'trigger' },
{ keyword: 'external', source: 'api' }]
possibilities.each do |possibility|
config = YAML.dump({
before_script: ["pwd"],
rspec: { script: "rspec", type: type, only: [possibility[:keyword]] }
})
config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.pipeline_stage_builds(type, pipeline(ref: 'deploy', tag: false, source: possibility[:source])).size).to eq(0) it do
expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
'jobs:rspec:only config should be an array of strings or regexps')
end end
end end
it "returns builds if only has current repository path" do context 'when it is invalid regex' do
seed_pipeline = pipeline(ref: 'deploy') let(:only) { ["/*invalid/"] }
config = YAML.dump({
before_script: ["pwd"],
rspec: {
script: "rspec",
type: type,
only: ["branches@#{seed_pipeline.project_full_path}"]
}
})
config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.pipeline_stage_builds(type, seed_pipeline).size).to eq(1) it do
end expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
'jobs:rspec:only config should be an array of strings or regexps')
it "does not return builds if only has different repository path" do
config = YAML.dump({
before_script: ["pwd"],
rspec: { script: "rspec", type: type, only: ["branches@fork"] }
})
config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(0)
end
it "returns build only for specified type" do
config = YAML.dump({
before_script: ["pwd"],
rspec: { script: "rspec", type: "test", only: %w(master deploy) },
staging: { script: "deploy", type: "deploy", only: %w(master deploy) },
production: { script: "deploy", type: "deploy", only: ["master@path", "deploy"] }
})
config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.pipeline_stage_builds("deploy", pipeline(ref: "deploy")).size).to eq(2)
expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "deploy")).size).to eq(1)
expect(config_processor.pipeline_stage_builds("deploy", pipeline(ref: "master")).size).to eq(1)
end
context 'for invalid value' do
let(:config) { { rspec: { script: "rspec", type: "test", only: only } } }
let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
context 'when it is integer' do
let(:only) { 1 }
it do
expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
'jobs:rspec:only has to be either an array of conditions or a hash')
end
end
context 'when it is an array of integers' do
let(:only) { [1, 1] }
it do
expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
'jobs:rspec:only config should be an array of strings or regexps')
end
end
context 'when it is invalid regex' do
let(:only) { ["/*invalid/"] }
it do
expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
'jobs:rspec:only config should be an array of strings or regexps')
end
end end
end end
end end
describe 'except' do context 'when `except` has an invalid value' do
it "returns builds if except has another branch" do let(:config) { { rspec: { script: "rspec", except: except } } }
config = YAML.dump({ let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
before_script: ["pwd"],
rspec: { script: "rspec", except: ["deploy"] }
})
config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(1)
end
it "returns builds if except has regexp with another branch" do
config = YAML.dump({
before_script: ["pwd"],
rspec: { script: "rspec", except: ["/^deploy$/"] }
})
config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(1) context 'when it is integer' do
end let(:except) { 1 }
it "does not return builds if except has specified this branch" do
config = YAML.dump({
before_script: ["pwd"],
rspec: { script: "rspec", except: ["master"] }
})
config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "master")).size).to eq(0)
end
it "does not return builds if except has a list of branches including specified" do
config = YAML.dump({
before_script: ["pwd"],
rspec: { script: "rspec", type: type, except: %w(master deploy) }
})
config_processor = Gitlab::Ci::YamlProcessor.new(config) it do
expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(0) 'jobs:rspec:except has to be either an array of conditions or a hash')
end
it "does not return builds if except has a branches keyword specified" do
config = YAML.dump({
before_script: ["pwd"],
rspec: { script: "rspec", type: type, except: ["branches"] }
})
config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(0)
end
it "returns builds if except has a tags keyword" do
config = YAML.dump({
before_script: ["pwd"],
rspec: { script: "rspec", type: type, except: ["tags"] }
})
config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(1)
end
it "does not return builds if except has special keywords specified and source matches" do
possibilities = [{ keyword: 'pushes', source: 'push' },
{ keyword: 'web', source: 'web' },
{ keyword: 'triggers', source: 'trigger' },
{ keyword: 'schedules', source: 'schedule' },
{ keyword: 'api', source: 'api' },
{ keyword: 'external', source: 'external' }]
possibilities.each do |possibility|
config = YAML.dump({
before_script: ["pwd"],
rspec: { script: "rspec", type: type, except: [possibility[:keyword]] }
})
config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.pipeline_stage_builds(type, pipeline(ref: 'deploy', tag: false, source: possibility[:source])).size).to eq(0)
end end
end end
it "returns builds if except has special keywords specified and source doesn't match" do context 'when it is an array of integers' do
possibilities = [{ keyword: 'pushes', source: 'web' }, let(:except) { [1, 1] }
{ keyword: 'web', source: 'push' },
{ keyword: 'triggers', source: 'schedule' },
{ keyword: 'schedules', source: 'external' },
{ keyword: 'api', source: 'trigger' },
{ keyword: 'external', source: 'api' }]
possibilities.each do |possibility|
config = YAML.dump({
before_script: ["pwd"],
rspec: { script: "rspec", type: type, except: [possibility[:keyword]] }
})
config_processor = Gitlab::Ci::YamlProcessor.new(config) it do
expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
expect(config_processor.pipeline_stage_builds(type, pipeline(ref: 'deploy', tag: false, source: possibility[:source])).size).to eq(1) 'jobs:rspec:except config should be an array of strings or regexps')
end end
end end
it "does not return builds if except has current repository path" do context 'when it is invalid regex' do
seed_pipeline = pipeline(ref: 'deploy') let(:except) { ["/*invalid/"] }
config = YAML.dump({
before_script: ["pwd"],
rspec: {
script: "rspec",
type: type,
except: ["branches@#{seed_pipeline.project_full_path}"]
}
})
config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.pipeline_stage_builds(type, seed_pipeline).size).to eq(0)
end
it "returns builds if except has different repository path" do
config = YAML.dump({
before_script: ["pwd"],
rspec: { script: "rspec", type: type, except: ["branches@fork"] }
})
config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.pipeline_stage_builds(type, pipeline(ref: "deploy")).size).to eq(1)
end
it "returns build except specified type" do
master_pipeline = pipeline(ref: 'master')
test_pipeline = pipeline(ref: 'test')
deploy_pipeline = pipeline(ref: 'deploy')
config = YAML.dump({
before_script: ["pwd"],
rspec: { script: "rspec", type: "test", except: ["master", "deploy", "test@#{test_pipeline.project_full_path}"] },
staging: { script: "deploy", type: "deploy", except: ["master"] },
production: { script: "deploy", type: "deploy", except: ["master@#{master_pipeline.project_full_path}"] }
})
config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.pipeline_stage_builds("deploy", deploy_pipeline).size).to eq(2)
expect(config_processor.pipeline_stage_builds("test", test_pipeline).size).to eq(0)
expect(config_processor.pipeline_stage_builds("deploy", master_pipeline).size).to eq(0)
end
context 'for invalid value' do
let(:config) { { rspec: { script: "rspec", except: except } } }
let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
context 'when it is integer' do it do
let(:except) { 1 } expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
'jobs:rspec:except config should be an array of strings or regexps')
it do
expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
'jobs:rspec:except has to be either an array of conditions or a hash')
end
end
context 'when it is an array of integers' do
let(:except) { [1, 1] }
it do
expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
'jobs:rspec:except config should be an array of strings or regexps')
end
end
context 'when it is invalid regex' do
let(:except) { ["/*invalid/"] }
it do
expect { processor }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
'jobs:rspec:except config should be an array of strings or regexps')
end
end end
end end
end end
...@@ -620,7 +254,7 @@ module Gitlab ...@@ -620,7 +254,7 @@ module Gitlab
let(:config_data) { YAML.dump(config) } let(:config_data) { YAML.dump(config) }
let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config_data) } let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config_data) }
subject { config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first } subject { config_processor.stage_builds_attributes('test').first }
describe "before_script" do describe "before_script" do
context "in global context" do context "in global context" do
...@@ -703,8 +337,8 @@ module Gitlab ...@@ -703,8 +337,8 @@ module Gitlab
config_processor = Gitlab::Ci::YamlProcessor.new(config) config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1) expect(config_processor.stage_builds_attributes("test").size).to eq(1)
expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({ expect(config_processor.stage_builds_attributes("test").first).to eq({
stage: "test", stage: "test",
stage_idx: 1, stage_idx: 1,
name: "rspec", name: "rspec",
...@@ -738,8 +372,8 @@ module Gitlab ...@@ -738,8 +372,8 @@ module Gitlab
config_processor = Gitlab::Ci::YamlProcessor.new(config) config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1) expect(config_processor.stage_builds_attributes("test").size).to eq(1)
expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({ expect(config_processor.stage_builds_attributes("test").first).to eq({
stage: "test", stage: "test",
stage_idx: 1, stage_idx: 1,
name: "rspec", name: "rspec",
...@@ -771,8 +405,8 @@ module Gitlab ...@@ -771,8 +405,8 @@ module Gitlab
config_processor = Gitlab::Ci::YamlProcessor.new(config) config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1) expect(config_processor.stage_builds_attributes("test").size).to eq(1)
expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({ expect(config_processor.stage_builds_attributes("test").first).to eq({
stage: "test", stage: "test",
stage_idx: 1, stage_idx: 1,
name: "rspec", name: "rspec",
...@@ -800,8 +434,8 @@ module Gitlab ...@@ -800,8 +434,8 @@ module Gitlab
config_processor = Gitlab::Ci::YamlProcessor.new(config) config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1) expect(config_processor.stage_builds_attributes("test").size).to eq(1)
expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({ expect(config_processor.stage_builds_attributes("test").first).to eq({
stage: "test", stage: "test",
stage_idx: 1, stage_idx: 1,
name: "rspec", name: "rspec",
...@@ -946,8 +580,8 @@ module Gitlab ...@@ -946,8 +580,8 @@ module Gitlab
}) })
config_processor = Gitlab::Ci::YamlProcessor.new(config) config_processor = Gitlab::Ci::YamlProcessor.new(config)
builds = config_processor.stage_builds_attributes("test")
builds = config_processor.pipeline_stage_builds("test", pipeline(ref: "master"))
expect(builds.size).to eq(1) expect(builds.size).to eq(1)
expect(builds.first[:when]).to eq(when_state) expect(builds.first[:when]).to eq(when_state)
end end
...@@ -978,8 +612,8 @@ module Gitlab ...@@ -978,8 +612,8 @@ module Gitlab
config_processor = Gitlab::Ci::YamlProcessor.new(config) config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1) expect(config_processor.stage_builds_attributes("test").size).to eq(1)
expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first[:options][:cache]).to eq( expect(config_processor.stage_builds_attributes("test").first[:options][:cache]).to eq(
paths: ["logs/", "binaries/"], paths: ["logs/", "binaries/"],
untracked: true, untracked: true,
key: 'key', key: 'key',
...@@ -997,8 +631,8 @@ module Gitlab ...@@ -997,8 +631,8 @@ module Gitlab
config_processor = Gitlab::Ci::YamlProcessor.new(config) config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1) expect(config_processor.stage_builds_attributes("test").size).to eq(1)
expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first[:options][:cache]).to eq( expect(config_processor.stage_builds_attributes("test").first[:options][:cache]).to eq(
paths: ["logs/", "binaries/"], paths: ["logs/", "binaries/"],
untracked: true, untracked: true,
key: 'key', key: 'key',
...@@ -1017,8 +651,8 @@ module Gitlab ...@@ -1017,8 +651,8 @@ module Gitlab
config_processor = Gitlab::Ci::YamlProcessor.new(config) config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1) expect(config_processor.stage_builds_attributes("test").size).to eq(1)
expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first[:options][:cache]).to eq( expect(config_processor.stage_builds_attributes("test").first[:options][:cache]).to eq(
paths: ["test/"], paths: ["test/"],
untracked: false, untracked: false,
key: 'local', key: 'local',
...@@ -1046,8 +680,8 @@ module Gitlab ...@@ -1046,8 +680,8 @@ module Gitlab
config_processor = Gitlab::Ci::YamlProcessor.new(config) config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).size).to eq(1) expect(config_processor.stage_builds_attributes("test").size).to eq(1)
expect(config_processor.pipeline_stage_builds("test", pipeline(ref: "master")).first).to eq({ expect(config_processor.stage_builds_attributes("test").first).to eq({
stage: "test", stage: "test",
stage_idx: 1, stage_idx: 1,
name: "rspec", name: "rspec",
...@@ -1083,8 +717,8 @@ module Gitlab ...@@ -1083,8 +717,8 @@ module Gitlab
}) })
config_processor = Gitlab::Ci::YamlProcessor.new(config) config_processor = Gitlab::Ci::YamlProcessor.new(config)
builds = config_processor.stage_builds_attributes("test")
builds = config_processor.pipeline_stage_builds("test", pipeline(ref: "master"))
expect(builds.size).to eq(1) expect(builds.size).to eq(1)
expect(builds.first[:options][:artifacts][:when]).to eq(when_state) expect(builds.first[:options][:artifacts][:when]).to eq(when_state)
end end
...@@ -1099,7 +733,7 @@ module Gitlab ...@@ -1099,7 +733,7 @@ module Gitlab
end end
let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) } let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
let(:builds) { processor.pipeline_stage_builds('deploy', pipeline(ref: 'master')) } let(:builds) { processor.stage_builds_attributes('deploy') }
context 'when a production environment is specified' do context 'when a production environment is specified' do
let(:environment) { 'production' } let(:environment) { 'production' }
...@@ -1256,7 +890,7 @@ module Gitlab ...@@ -1256,7 +890,7 @@ module Gitlab
describe "Hidden jobs" do describe "Hidden jobs" do
let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config) } let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config) }
subject { config_processor.pipeline_stage_builds("test", pipeline(ref: "master")) } subject { config_processor.stage_builds_attributes("test") }
shared_examples 'hidden_job_handling' do shared_examples 'hidden_job_handling' do
it "doesn't create jobs that start with dot" do it "doesn't create jobs that start with dot" do
...@@ -1304,7 +938,7 @@ module Gitlab ...@@ -1304,7 +938,7 @@ module Gitlab
describe "YAML Alias/Anchor" do describe "YAML Alias/Anchor" do
let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config) } let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config) }
subject { config_processor.pipeline_stage_builds("build", pipeline(ref: "master")) } subject { config_processor.stage_builds_attributes("build") }
shared_examples 'job_templates_handling' do shared_examples 'job_templates_handling' do
it "is correctly supported for jobs" do it "is correctly supported for jobs" do
...@@ -1344,13 +978,13 @@ module Gitlab ...@@ -1344,13 +978,13 @@ module Gitlab
context 'when template is a job' do context 'when template is a job' do
let(:config) do let(:config) do
<<EOT <<~EOT
job1: &JOBTMPL job1: &JOBTMPL
stage: build stage: build
script: execute-script-for-job script: execute-script-for-job
job2: *JOBTMPL job2: *JOBTMPL
EOT EOT
end end
it_behaves_like 'job_templates_handling' it_behaves_like 'job_templates_handling'
...@@ -1358,15 +992,15 @@ EOT ...@@ -1358,15 +992,15 @@ EOT
context 'when template is a hidden job' do context 'when template is a hidden job' do
let(:config) do let(:config) do
<<EOT <<~EOT
.template: &JOBTMPL .template: &JOBTMPL
stage: build stage: build
script: execute-script-for-job script: execute-script-for-job
job1: *JOBTMPL job1: *JOBTMPL
job2: *JOBTMPL job2: *JOBTMPL
EOT EOT
end end
it_behaves_like 'job_templates_handling' it_behaves_like 'job_templates_handling'
...@@ -1374,18 +1008,18 @@ EOT ...@@ -1374,18 +1008,18 @@ EOT
context 'when job adds its own keys to a template definition' do context 'when job adds its own keys to a template definition' do
let(:config) do let(:config) do
<<EOT <<~EOT
.template: &JOBTMPL .template: &JOBTMPL
stage: build stage: build
job1: job1:
<<: *JOBTMPL <<: *JOBTMPL
script: execute-script-for-job script: execute-script-for-job
job2: job2:
<<: *JOBTMPL <<: *JOBTMPL
script: execute-script-for-job script: execute-script-for-job
EOT EOT
end end
it_behaves_like 'job_templates_handling' it_behaves_like 'job_templates_handling'
...@@ -1724,10 +1358,6 @@ EOT ...@@ -1724,10 +1358,6 @@ EOT
it { is_expected.to be_nil } it { is_expected.to be_nil }
end end
end end
def pipeline(**attributes)
build_stubbed(:ci_empty_pipeline, **attributes)
end
end end
end end
end end
...@@ -177,6 +177,24 @@ describe Ci::Pipeline, :mailer do ...@@ -177,6 +177,24 @@ describe Ci::Pipeline, :mailer do
end end
end end
describe '#protected_ref?' do
it 'delegates method to project' do
expect(pipeline).not_to be_protected_ref
end
end
describe '#legacy_trigger' do
let(:trigger_request) { create(:ci_trigger_request) }
before do
pipeline.trigger_requests << trigger_request
end
it 'returns first trigger request' do
expect(pipeline.legacy_trigger).to eq trigger_request
end
end
describe '#auto_canceled?' do describe '#auto_canceled?' do
subject { pipeline.auto_canceled? } subject { pipeline.auto_canceled? }
...@@ -215,142 +233,257 @@ describe Ci::Pipeline, :mailer do ...@@ -215,142 +233,257 @@ describe Ci::Pipeline, :mailer do
end end
describe 'pipeline stages' do describe 'pipeline stages' do
before do
create(:commit_status, pipeline: pipeline,
stage: 'build',
name: 'linux',
stage_idx: 0,
status: 'success')
create(:commit_status, pipeline: pipeline,
stage: 'build',
name: 'mac',
stage_idx: 0,
status: 'failed')
create(:commit_status, pipeline: pipeline,
stage: 'deploy',
name: 'staging',
stage_idx: 2,
status: 'running')
create(:commit_status, pipeline: pipeline,
stage: 'test',
name: 'rspec',
stage_idx: 1,
status: 'success')
end
describe '#stage_seeds' do describe '#stage_seeds' do
let(:pipeline) do let(:pipeline) { build(:ci_pipeline, config: config) }
build(:ci_pipeline, config: { rspec: { script: 'rake' } }) let(:config) { { rspec: { script: 'rake' } } }
end
it 'returns preseeded stage seeds object' do it 'returns preseeded stage seeds object' do
expect(pipeline.stage_seeds).to all(be_a Gitlab::Ci::Stage::Seed) expect(pipeline.stage_seeds)
.to all(be_a Gitlab::Ci::Pipeline::Seed::Base)
expect(pipeline.stage_seeds.count).to eq 1 expect(pipeline.stage_seeds.count).to eq 1
end end
end
describe '#seeds_size' do context 'when no refs policy is specified' do
let(:pipeline) { build(:ci_pipeline_with_one_job) } let(:config) do
{ production: { stage: 'deploy', script: 'cap prod' },
rspec: { stage: 'test', script: 'rspec' },
spinach: { stage: 'test', script: 'spinach' } }
end
it 'returns number of jobs in stage seeds' do it 'correctly fabricates a stage seeds object' do
expect(pipeline.seeds_size).to eq 1 seeds = pipeline.stage_seeds
expect(seeds.size).to eq 2
expect(seeds.first.attributes[:name]).to eq 'test'
expect(seeds.second.attributes[:name]).to eq 'deploy'
expect(seeds.dig(0, 0, :name)).to eq 'rspec'
expect(seeds.dig(0, 1, :name)).to eq 'spinach'
expect(seeds.dig(1, 0, :name)).to eq 'production'
end
end end
end
describe '#legacy_stages' do context 'when refs policy is specified' do
subject { pipeline.legacy_stages } let(:pipeline) do
build(:ci_pipeline, ref: 'feature', tag: true, config: config)
end
context 'stages list' do let(:config) do
it 'returns ordered list of stages' do { production: { stage: 'deploy', script: 'cap prod', only: ['master'] },
expect(subject.map(&:name)).to eq(%w[build test deploy]) spinach: { stage: 'test', script: 'spinach', only: ['tags'] } }
end
it 'returns stage seeds only assigned to master to master' do
seeds = pipeline.stage_seeds
expect(seeds.size).to eq 1
expect(seeds.first.attributes[:name]).to eq 'test'
expect(seeds.dig(0, 0, :name)).to eq 'spinach'
end end
end end
context 'stages with statuses' do context 'when source policy is specified' do
let(:statuses) do let(:pipeline) { build(:ci_pipeline, source: :schedule, config: config) }
subject.map { |stage| [stage.name, stage.status] }
let(:config) do
{ production: { stage: 'deploy', script: 'cap prod', only: ['triggers'] },
spinach: { stage: 'test', script: 'spinach', only: ['schedules'] } }
end end
it 'returns list of stages with correct statuses' do it 'returns stage seeds only assigned to schedules' do
expect(statuses).to eq([%w(build failed), seeds = pipeline.stage_seeds
%w(test success),
%w(deploy running)]) expect(seeds.size).to eq 1
expect(seeds.first.attributes[:name]).to eq 'test'
expect(seeds.dig(0, 0, :name)).to eq 'spinach'
end end
end
context 'when commit status is retried' do context 'when kubernetes policy is specified' do
before do let(:config) do
create(:commit_status, pipeline: pipeline, {
stage: 'build', spinach: { stage: 'test', script: 'spinach' },
name: 'mac', production: {
stage_idx: 0, stage: 'deploy',
status: 'success') script: 'cap',
only: { kubernetes: 'active' }
}
}
end
context 'when kubernetes is active' do
shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do
it 'returns seeds for kubernetes dependent job' do
seeds = pipeline.stage_seeds
pipeline.process! expect(seeds.size).to eq 2
expect(seeds.dig(0, 0, :name)).to eq 'spinach'
expect(seeds.dig(1, 0, :name)).to eq 'production'
end
end end
it 'ignores the previous state' do context 'when user configured kubernetes from Integration > Kubernetes' do
expect(statuses).to eq([%w(build success), let(:project) { create(:kubernetes_project) }
%w(test success), let(:pipeline) { build(:ci_pipeline, project: project, config: config) }
%w(deploy running)])
it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
end end
end
end
context 'when there is a stage with warnings' do context 'when user configured kubernetes from CI/CD > Clusters' do
before do let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
create(:commit_status, pipeline: pipeline, let(:project) { cluster.project }
stage: 'deploy', let(:pipeline) { build(:ci_pipeline, project: project, config: config) }
name: 'prod:2',
stage_idx: 2, it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes'
status: 'failed', end
allow_failure: true)
end end
it 'populates stage with correct number of warnings' do context 'when kubernetes is not active' do
deploy_stage = pipeline.legacy_stages.third it 'does not return seeds for kubernetes dependent job' do
seeds = pipeline.stage_seeds
expect(deploy_stage).not_to receive(:statuses) expect(seeds.size).to eq 1
expect(deploy_stage).to have_warnings expect(seeds.dig(0, 0, :name)).to eq 'spinach'
end
end end
end end
end end
describe '#stages_count' do describe '#seeds_size' do
it 'returns a valid number of stages' do context 'when refs policy is specified' do
expect(pipeline.stages_count).to eq(3) let(:config) do
end { production: { stage: 'deploy', script: 'cap prod', only: ['master'] },
end spinach: { stage: 'test', script: 'spinach', only: ['tags'] } }
end
describe '#stages_names' do let(:pipeline) do
it 'returns a valid names of stages' do build(:ci_pipeline, ref: 'feature', tag: true, config: config)
expect(pipeline.stages_names).to eq(%w(build test deploy)) end
it 'returns real seeds size' do
expect(pipeline.seeds_size).to eq 1
end
end end
end end
end
describe '#legacy_stage' do
subject { pipeline.legacy_stage('test') }
context 'with status in stage' do describe 'legacy stages' do
before do before do
create(:commit_status, pipeline: pipeline, stage: 'test') create(:commit_status, pipeline: pipeline,
stage: 'build',
name: 'linux',
stage_idx: 0,
status: 'success')
create(:commit_status, pipeline: pipeline,
stage: 'build',
name: 'mac',
stage_idx: 0,
status: 'failed')
create(:commit_status, pipeline: pipeline,
stage: 'deploy',
name: 'staging',
stage_idx: 2,
status: 'running')
create(:commit_status, pipeline: pipeline,
stage: 'test',
name: 'rspec',
stage_idx: 1,
status: 'success')
end
describe '#legacy_stages' do
subject { pipeline.legacy_stages }
context 'stages list' do
it 'returns ordered list of stages' do
expect(subject.map(&:name)).to eq(%w[build test deploy])
end
end
context 'stages with statuses' do
let(:statuses) do
subject.map { |stage| [stage.name, stage.status] }
end
it 'returns list of stages with correct statuses' do
expect(statuses).to eq([%w(build failed),
%w(test success),
%w(deploy running)])
end
context 'when commit status is retried' do
before do
create(:commit_status, pipeline: pipeline,
stage: 'build',
name: 'mac',
stage_idx: 0,
status: 'success')
pipeline.process!
end
it 'ignores the previous state' do
expect(statuses).to eq([%w(build success),
%w(test success),
%w(deploy running)])
end
end
end
context 'when there is a stage with warnings' do
before do
create(:commit_status, pipeline: pipeline,
stage: 'deploy',
name: 'prod:2',
stage_idx: 2,
status: 'failed',
allow_failure: true)
end
it 'populates stage with correct number of warnings' do
deploy_stage = pipeline.legacy_stages.third
expect(deploy_stage).not_to receive(:statuses)
expect(deploy_stage).to have_warnings
end
end
end
describe '#stages_count' do
it 'returns a valid number of stages' do
expect(pipeline.stages_count).to eq(3)
end
end end
it { expect(subject).to be_a Ci::LegacyStage } describe '#stages_names' do
it { expect(subject.name).to eq 'test' } it 'returns a valid names of stages' do
it { expect(subject.statuses).not_to be_empty } expect(pipeline.stages_names).to eq(%w(build test deploy))
end
end
end end
context 'without status in stage' do describe '#legacy_stage' do
before do subject { pipeline.legacy_stage('test') }
create(:commit_status, pipeline: pipeline, stage: 'build')
context 'with status in stage' do
before do
create(:commit_status, pipeline: pipeline, stage: 'test')
end
it { expect(subject).to be_a Ci::LegacyStage }
it { expect(subject.name).to eq 'test' }
it { expect(subject.statuses).not_to be_empty }
end end
it 'return stage object' do context 'without status in stage' do
is_expected.to be_nil before do
create(:commit_status, pipeline: pipeline, stage: 'build')
end
it 'return stage object' do
is_expected.to be_nil
end
end end
end end
end end
...@@ -589,20 +722,6 @@ describe Ci::Pipeline, :mailer do ...@@ -589,20 +722,6 @@ describe Ci::Pipeline, :mailer do
end end
end end
describe '#has_stage_seeds?' do
context 'when pipeline has stage seeds' do
subject { build(:ci_pipeline_with_one_job) }
it { is_expected.to have_stage_seeds }
end
context 'when pipeline does not have stage seeds' do
subject { create(:ci_pipeline_without_jobs) }
it { is_expected.not_to have_stage_seeds }
end
end
describe '#has_warnings?' do describe '#has_warnings?' do
subject { pipeline.has_warnings? } subject { pipeline.has_warnings? }
......
...@@ -5,6 +5,7 @@ describe 'ci/lints/show' do ...@@ -5,6 +5,7 @@ describe 'ci/lints/show' do
describe 'XSS protection' do describe 'XSS protection' do
let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(content)) } let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(content)) }
before do before do
assign(:status, true) assign(:status, true)
assign(:builds, config_processor.builds) assign(:builds, config_processor.builds)
......
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