Commit b097d065 authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch '5836-move-lib-ci-into-gitlab-namespace' into 'master'

Resolve "Move `lib/ci` to `lib/gitlab/ci`"

Closes #5836

See merge request !14078
parents 373ff978 e83a8187
......@@ -7,11 +7,11 @@ module Ci
def create
@content = params[:content]
@error = Ci::GitlabCiYamlProcessor.validation_message(@content)
@error = Gitlab::Ci::YamlProcessor.validation_message(@content)
@status = @error.blank?
if @error.blank?
@config_processor = Ci::GitlabCiYamlProcessor.new(@content)
@config_processor = Gitlab::Ci::YamlProcessor.new(@content)
@stages = @config_processor.stages
@builds = @config_processor.builds
@jobs = @config_processor.jobs
......
......@@ -132,10 +132,10 @@ class Projects::PipelinesController < Projects::ApplicationController
def charts
@charts = {}
@charts[:week] = Ci::Charts::WeekChart.new(project)
@charts[:month] = Ci::Charts::MonthChart.new(project)
@charts[:year] = Ci::Charts::YearChart.new(project)
@charts[:pipeline_times] = Ci::Charts::PipelineTime.new(project)
@charts[:week] = Gitlab::Ci::Charts::WeekChart.new(project)
@charts[:month] = Gitlab::Ci::Charts::MonthChart.new(project)
@charts[:year] = Gitlab::Ci::Charts::YearChart.new(project)
@charts[:pipeline_times] = Gitlab::Ci::Charts::PipelineTime.new(project)
@counts = {}
@counts[:total] = @project.pipelines.count(:all)
......
......@@ -13,7 +13,7 @@ module BlobViewer
prepare!
@validation_message = Ci::GitlabCiYamlProcessor.validation_message(blob.data)
@validation_message = Gitlab::Ci::YamlProcessor.validation_message(blob.data)
end
def valid?
......
......@@ -446,8 +446,8 @@ module Ci
return unless trace
trace = trace.dup
Ci::MaskSecret.mask!(trace, project.runners_token) if project
Ci::MaskSecret.mask!(trace, token)
Gitlab::Ci::MaskSecret.mask!(trace, project.runners_token) if project
Gitlab::Ci::MaskSecret.mask!(trace, token)
trace
end
......
module Ci
class GroupVariable < ActiveRecord::Base
extend Ci::Model
extend Gitlab::Ci::Model
include HasVariable
include Presentable
......
module Ci
class Pipeline < ActiveRecord::Base
extend Ci::Model
extend Gitlab::Ci::Model
include HasStatus
include Importable
include AfterCommitQueue
......@@ -336,8 +336,8 @@ module Ci
return @config_processor if defined?(@config_processor)
@config_processor ||= begin
Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.full_path)
rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
Gitlab::Ci::YamlProcessor.new(ci_yaml_file, project.full_path)
rescue Gitlab::Ci::YamlProcessor::ValidationError, Psych::SyntaxError => e
self.yaml_errors = e.message
nil
rescue
......
module Ci
class PipelineSchedule < ActiveRecord::Base
extend Ci::Model
extend Gitlab::Ci::Model
include Importable
acts_as_paranoid
......
module Ci
class PipelineScheduleVariable < ActiveRecord::Base
extend Ci::Model
extend Gitlab::Ci::Model
include HasVariable
belongs_to :pipeline_schedule
......
module Ci
class PipelineVariable < ActiveRecord::Base
extend Ci::Model
extend Gitlab::Ci::Model
include HasVariable
belongs_to :pipeline
......
module Ci
class Runner < ActiveRecord::Base
extend Ci::Model
extend Gitlab::Ci::Model
RUNNER_QUEUE_EXPIRY_TIME = 60.minutes
ONLINE_CONTACT_TIMEOUT = 1.hour
......
module Ci
class RunnerProject < ActiveRecord::Base
extend Ci::Model
extend Gitlab::Ci::Model
belongs_to :runner
belongs_to :project
......
module Ci
class Stage < ActiveRecord::Base
extend Ci::Model
extend Gitlab::Ci::Model
include Importable
include HasStatus
include Gitlab::OptimisticLocking
......
module Ci
class Trigger < ActiveRecord::Base
extend Ci::Model
extend Gitlab::Ci::Model
acts_as_paranoid
......
module Ci
class TriggerRequest < ActiveRecord::Base
extend Ci::Model
extend Gitlab::Ci::Model
belongs_to :trigger
belongs_to :pipeline, foreign_key: :commit_id
......
module Ci
class Variable < ActiveRecord::Base
extend Ci::Model
extend Gitlab::Ci::Model
include HasVariable
include Presentable
......
---
title: Move `lib/ci` to `lib/gitlab/ci`
merge_request: 14078
author: Maxim Rydkin
type: other
......@@ -6,7 +6,7 @@ module API
requires :content, type: String, desc: 'Content of .gitlab-ci.yml'
end
post '/lint' do
error = Ci::GitlabCiYamlProcessor.validation_message(params[:content])
error = Gitlab::Ci::YamlProcessor.validation_message(params[:content])
status 200
......
# ANSI color library
#
# Implementation per http://en.wikipedia.org/wiki/ANSI_escape_code
module Ci
module Ansi2html
# keys represent the trailing digit in color changing command (30-37, 40-47, 90-97. 100-107)
COLOR = {
0 => 'black', # not that this is gray in the intense color table
1 => 'red',
2 => 'green',
3 => 'yellow',
4 => 'blue',
5 => 'magenta',
6 => 'cyan',
7 => 'white', # not that this is gray in the dark (aka default) color table
}.freeze
STYLE_SWITCHES = {
bold: 0x01,
italic: 0x02,
underline: 0x04,
conceal: 0x08,
cross: 0x10
}.freeze
def self.convert(ansi, state = nil)
Converter.new.convert(ansi, state)
end
class Converter
def on_0(s) reset() end
def on_1(s) enable(STYLE_SWITCHES[:bold]) end
def on_3(s) enable(STYLE_SWITCHES[:italic]) end
def on_4(s) enable(STYLE_SWITCHES[:underline]) end
def on_8(s) enable(STYLE_SWITCHES[:conceal]) end
def on_9(s) enable(STYLE_SWITCHES[:cross]) end
def on_21(s) disable(STYLE_SWITCHES[:bold]) end
def on_22(s) disable(STYLE_SWITCHES[:bold]) end
def on_23(s) disable(STYLE_SWITCHES[:italic]) end
def on_24(s) disable(STYLE_SWITCHES[:underline]) end
def on_28(s) disable(STYLE_SWITCHES[:conceal]) end
def on_29(s) disable(STYLE_SWITCHES[:cross]) end
def on_30(s) set_fg_color(0) end
def on_31(s) set_fg_color(1) end
def on_32(s) set_fg_color(2) end
def on_33(s) set_fg_color(3) end
def on_34(s) set_fg_color(4) end
def on_35(s) set_fg_color(5) end
def on_36(s) set_fg_color(6) end
def on_37(s) set_fg_color(7) end
def on_38(s) set_fg_color_256(s) end
def on_39(s) set_fg_color(9) end
def on_40(s) set_bg_color(0) end
def on_41(s) set_bg_color(1) end
def on_42(s) set_bg_color(2) end
def on_43(s) set_bg_color(3) end
def on_44(s) set_bg_color(4) end
def on_45(s) set_bg_color(5) end
def on_46(s) set_bg_color(6) end
def on_47(s) set_bg_color(7) end
def on_48(s) set_bg_color_256(s) end
def on_49(s) set_bg_color(9) end
def on_90(s) set_fg_color(0, 'l') end
def on_91(s) set_fg_color(1, 'l') end
def on_92(s) set_fg_color(2, 'l') end
def on_93(s) set_fg_color(3, 'l') end
def on_94(s) set_fg_color(4, 'l') end
def on_95(s) set_fg_color(5, 'l') end
def on_96(s) set_fg_color(6, 'l') end
def on_97(s) set_fg_color(7, 'l') end
def on_99(s) set_fg_color(9, 'l') end
def on_100(s) set_bg_color(0, 'l') end
def on_101(s) set_bg_color(1, 'l') end
def on_102(s) set_bg_color(2, 'l') end
def on_103(s) set_bg_color(3, 'l') end
def on_104(s) set_bg_color(4, 'l') end
def on_105(s) set_bg_color(5, 'l') end
def on_106(s) set_bg_color(6, 'l') end
def on_107(s) set_bg_color(7, 'l') end
def on_109(s) set_bg_color(9, 'l') end
attr_accessor :offset, :n_open_tags, :fg_color, :bg_color, :style_mask
STATE_PARAMS = [:offset, :n_open_tags, :fg_color, :bg_color, :style_mask].freeze
def convert(stream, new_state)
reset_state
restore_state(new_state, stream) if new_state.present?
append = false
truncated = false
cur_offset = stream.tell
if cur_offset > @offset
@offset = cur_offset
truncated = true
else
stream.seek(@offset)
append = @offset > 0
end
start_offset = @offset
open_new_tag
stream.each_line do |line|
s = StringScanner.new(line)
until s.eos?
if s.scan(/\e([@-_])(.*?)([@-~])/)
handle_sequence(s)
elsif s.scan(/\e(([@-_])(.*?)?)?$/)
break
elsif s.scan(/</)
@out << '&lt;'
elsif s.scan(/\r?\n/)
@out << '<br>'
else
@out << s.scan(/./m)
end
@offset += s.matched_size
end
end
close_open_tags()
OpenStruct.new(
html: @out.force_encoding(Encoding.default_external),
state: state,
append: append,
truncated: truncated,
offset: start_offset,
size: stream.tell - start_offset,
total: stream.size
)
end
def handle_sequence(s)
indicator = s[1]
commands = s[2].split ';'
terminator = s[3]
# We are only interested in color and text style changes - triggered by
# sequences starting with '\e[' and ending with 'm'. Any other control
# sequence gets stripped (including stuff like "delete last line")
return unless indicator == '[' && terminator == 'm'
close_open_tags()
if commands.empty?()
reset()
return
end
evaluate_command_stack(commands)
open_new_tag
end
def evaluate_command_stack(stack)
return unless command = stack.shift()
if self.respond_to?("on_#{command}", true)
self.__send__("on_#{command}", stack) # rubocop:disable GitlabSecurity/PublicSend
end
evaluate_command_stack(stack)
end
def open_new_tag
css_classes = []
unless @fg_color.nil?
fg_color = @fg_color
# Most terminals show bold colored text in the light color variant
# Let's mimic that here
if @style_mask & STYLE_SWITCHES[:bold] != 0
fg_color.sub!(/fg-(\w{2,}+)/, 'fg-l-\1')
end
css_classes << fg_color
end
css_classes << @bg_color unless @bg_color.nil?
STYLE_SWITCHES.each do |css_class, flag|
css_classes << "term-#{css_class}" if @style_mask & flag != 0
end
return if css_classes.empty?
@out << %{<span class="#{css_classes.join(' ')}">}
@n_open_tags += 1
end
def close_open_tags
while @n_open_tags > 0
@out << %{</span>}
@n_open_tags -= 1
end
end
def reset_state
@offset = 0
@n_open_tags = 0
@out = ''
reset
end
def state
state = STATE_PARAMS.inject({}) do |h, param|
h[param] = send(param) # rubocop:disable GitlabSecurity/PublicSend
h
end
Base64.urlsafe_encode64(state.to_json)
end
def restore_state(new_state, stream)
state = Base64.urlsafe_decode64(new_state)
state = JSON.parse(state, symbolize_names: true)
return if state[:offset].to_i > stream.size
STATE_PARAMS.each do |param|
send("#{param}=".to_sym, state[param]) # rubocop:disable GitlabSecurity/PublicSend
end
end
def reset
@fg_color = nil
@bg_color = nil
@style_mask = 0
end
def enable(flag)
@style_mask |= flag
end
def disable(flag)
@style_mask &= ~flag
end
def set_fg_color(color_index, prefix = nil)
@fg_color = get_term_color_class(color_index, ["fg", prefix])
end
def set_bg_color(color_index, prefix = nil)
@bg_color = get_term_color_class(color_index, ["bg", prefix])
end
def get_term_color_class(color_index, prefix)
color_name = COLOR[color_index]
return nil if color_name.nil?
get_color_class(["term", prefix, color_name])
end
def set_fg_color_256(command_stack)
css_class = get_xterm_color_class(command_stack, "fg")
@fg_color = css_class unless css_class.nil?
end
def set_bg_color_256(command_stack)
css_class = get_xterm_color_class(command_stack, "bg")
@bg_color = css_class unless css_class.nil?
end
def get_xterm_color_class(command_stack, prefix)
# the 38 and 48 commands have to be followed by "5" and the color index
return unless command_stack.length >= 2
return unless command_stack[0] == "5"
command_stack.shift() # ignore the "5" command
color_index = command_stack.shift().to_i
return unless color_index >= 0
return unless color_index <= 255
get_color_class(["xterm", prefix, color_index])
end
def get_color_class(segments)
[segments].flatten.compact.join('-')
end
end
end
end
module Ci
module Charts
module DailyInterval
def grouped_count(query)
query
.group("DATE(#{Ci::Pipeline.table_name}.created_at)")
.count(:created_at)
.transform_keys { |date| date.strftime(@format) }
end
def interval_step
@interval_step ||= 1.day
end
end
module MonthlyInterval
def grouped_count(query)
if Gitlab::Database.postgresql?
query
.group("to_char(#{Ci::Pipeline.table_name}.created_at, '01 Month YYYY')")
.count(:created_at)
.transform_keys(&:squish)
else
query
.group("DATE_FORMAT(#{Ci::Pipeline.table_name}.created_at, '01 %M %Y')")
.count(:created_at)
end
end
def interval_step
@interval_step ||= 1.month
end
end
class Chart
attr_reader :labels, :total, :success, :project, :pipeline_times
def initialize(project)
@labels = []
@total = []
@success = []
@pipeline_times = []
@project = project
collect
end
def collect
query = project.pipelines
.where("? > #{Ci::Pipeline.table_name}.created_at AND #{Ci::Pipeline.table_name}.created_at > ?", @to, @from) # rubocop:disable GitlabSecurity/SqlInjection
totals_count = grouped_count(query)
success_count = grouped_count(query.success)
current = @from
while current < @to
label = current.strftime(@format)
@labels << label
@total << (totals_count[label] || 0)
@success << (success_count[label] || 0)
current += interval_step
end
end
end
class YearChart < Chart
include MonthlyInterval
def initialize(*)
@to = Date.today.end_of_month
@from = @to.years_ago(1).beginning_of_month
@format = '%d %B %Y'
super
end
end
class MonthChart < Chart
include DailyInterval
def initialize(*)
@to = Date.today
@from = @to - 30.days
@format = '%d %B'
super
end
end
class WeekChart < Chart
include DailyInterval
def initialize(*)
@to = Date.today
@from = @to - 7.days
@format = '%d %B'
super
end
end
class PipelineTime < Chart
def collect
commits = project.pipelines.last(30)
commits.each do |commit|
@labels << commit.short_sha
duration = commit.duration || 0
@pipeline_times << (duration / 60)
end
end
end
end
end
module Ci
class GitlabCiYamlProcessor
ValidationError = Class.new(StandardError)
include Gitlab::Ci::Config::Entry::LegacyValidationHelpers
attr_reader :path, :cache, :stages, :jobs
def initialize(config, path = nil)
@ci_config = Gitlab::Ci::Config.new(config)
@config = @ci_config.to_hash
@path = path
unless @ci_config.valid?
raise ValidationError, @ci_config.errors.first
end
initial_parsing
rescue Gitlab::Ci::Config::Loader::FormatError => e
raise ValidationError, e.message
end
def builds_for_stage_and_ref(stage, ref, tag = false, source = nil)
jobs_for_stage_and_ref(stage, ref, tag, source).map do |name, _|
build_attributes(name)
end
end
def builds
@jobs.map do |name, _|
build_attributes(name)
end
end
def stage_seeds(pipeline)
seeds = @stages.uniq.map do |stage|
builds = pipeline_stage_builds(stage, pipeline)
Gitlab::Ci::Stage::Seed.new(pipeline, stage, builds) if builds.any?
end
seeds.compact
end
def build_attributes(name)
job = @jobs[name.to_sym] || {}
{ stage_idx: @stages.index(job[:stage]),
stage: job[:stage],
commands: job[:commands],
tag_list: job[:tags] || [],
name: job[:name].to_s,
allow_failure: job[:ignore],
when: job[:when] || 'on_success',
environment: job[:environment_name],
coverage_regex: job[:coverage],
yaml_variables: yaml_variables(name),
options: {
image: job[:image],
services: job[:services],
artifacts: job[:artifacts],
cache: job[:cache],
dependencies: job[:dependencies],
before_script: job[:before_script],
script: job[:script],
after_script: job[:after_script],
environment: job[:environment],
retry: job[:retry]
}.compact }
end
def self.validation_message(content)
return 'Please provide content of .gitlab-ci.yml' if content.blank?
begin
Ci::GitlabCiYamlProcessor.new(content)
nil
rescue ValidationError, Psych::SyntaxError => e
e.message
end
end
private
def pipeline_stage_builds(stage, pipeline)
builds = builds_for_stage_and_ref(
stage, pipeline.ref, pipeline.tag?, pipeline.source)
builds.select do |build|
job = @jobs[build.fetch(:name).to_sym]
has_kubernetes = pipeline.has_kubernetes_active?
only_kubernetes = job.dig(:only, :kubernetes)
except_kubernetes = job.dig(:except, :kubernetes)
[!only_kubernetes && !except_kubernetes,
only_kubernetes && has_kubernetes,
except_kubernetes && !has_kubernetes].any?
end
end
def jobs_for_ref(ref, tag = false, source = nil)
@jobs.select do |_, job|
process?(job.dig(:only, :refs), job.dig(:except, :refs), ref, tag, source)
end
end
def jobs_for_stage_and_ref(stage, ref, tag = false, source = nil)
jobs_for_ref(ref, tag, source).select do |_, job|
job[:stage] == stage
end
end
def initial_parsing
##
# Global config
#
@before_script = @ci_config.before_script
@image = @ci_config.image
@after_script = @ci_config.after_script
@services = @ci_config.services
@variables = @ci_config.variables
@stages = @ci_config.stages
@cache = @ci_config.cache
##
# Jobs
#
@jobs = @ci_config.jobs
@jobs.each do |name, job|
# logical validation for job
validate_job_stage!(name, job)
validate_job_dependencies!(name, job)
validate_job_environment!(name, job)
end
end
def yaml_variables(name)
variables = (@variables || {})
.merge(job_variables(name))
variables.map do |key, value|
{ key: key.to_s, value: value, public: true }
end
end
def job_variables(name)
job = @jobs[name.to_sym]
return {} unless job
job[:variables] || {}
end
def validate_job_stage!(name, job)
return unless job[:stage]
unless job[:stage].is_a?(String) && job[:stage].in?(@stages)
raise ValidationError, "#{name} job: stage parameter should be #{@stages.join(", ")}"
end
end
def validate_job_dependencies!(name, job)
return unless job[:dependencies]
stage_index = @stages.index(job[:stage])
job[:dependencies].each do |dependency|
raise ValidationError, "#{name} job: undefined dependency: #{dependency}" unless @jobs[dependency.to_sym]
unless @stages.index(@jobs[dependency.to_sym][:stage]) < stage_index
raise ValidationError, "#{name} job: dependency #{dependency} is not defined in prior stages"
end
end
end
def validate_job_environment!(name, job)
return unless job[:environment]
return unless job[:environment].is_a?(Hash)
environment = job[:environment]
validate_on_stop_job!(name, environment, environment[:on_stop])
end
def validate_on_stop_job!(name, environment, on_stop)
return unless on_stop
on_stop_job = @jobs[on_stop.to_sym]
unless on_stop_job
raise ValidationError, "#{name} job: on_stop job #{on_stop} is not defined"
end
unless on_stop_job[:environment]
raise ValidationError, "#{name} job: on_stop job #{on_stop} does not have environment defined"
end
unless on_stop_job[:environment][:name] == environment[:name]
raise ValidationError, "#{name} job: on_stop job #{on_stop} have different environment name"
end
unless on_stop_job[:environment][:action] == 'stop'
raise ValidationError, "#{name} job: on_stop job #{on_stop} needs to have action stop defined"
end
end
def process?(only_params, except_params, ref, tag, source)
if only_params.present?
return false unless matching?(only_params, ref, tag, source)
end
if except_params.present?
return false if matching?(except_params, ref, tag, source)
end
true
end
def matching?(patterns, ref, tag, source)
patterns.any? do |pattern|
pattern, path = pattern.split('@', 2)
matches_path?(path) && matches_pattern?(pattern, ref, tag, source)
end
end
def matches_path?(path)
return true unless path
path == self.path
end
def matches_pattern?(pattern, ref, tag, source)
return true if tag && pattern == 'tags'
return true if !tag && pattern == 'branches'
return true if source_to_pattern(source) == pattern
if pattern.first == "/" && pattern.last == "/"
Regexp.new(pattern[1...-1]) =~ ref
else
pattern == ref
end
end
def source_to_pattern(source)
if %w[api external web].include?(source)
source
else
source&.pluralize
end
end
end
end
module Ci::MaskSecret
class << self
def mask!(value, token)
return value unless value.present? && token.present?
value.gsub!(token, 'x' * token.length)
value
end
end
end
module Ci
module Model
def table_name_prefix
"ci_"
end
def model_name
@model_name ||= ActiveModel::Name.new(self, nil, self.name.split("::").last)
end
end
end
# ANSI color library
#
# Implementation per http://en.wikipedia.org/wiki/ANSI_escape_code
module Gitlab
module Ci
module Ansi2html
# keys represent the trailing digit in color changing command (30-37, 40-47, 90-97. 100-107)
COLOR = {
0 => 'black', # not that this is gray in the intense color table
1 => 'red',
2 => 'green',
3 => 'yellow',
4 => 'blue',
5 => 'magenta',
6 => 'cyan',
7 => 'white', # not that this is gray in the dark (aka default) color table
}.freeze
STYLE_SWITCHES = {
bold: 0x01,
italic: 0x02,
underline: 0x04,
conceal: 0x08,
cross: 0x10
}.freeze
def self.convert(ansi, state = nil)
Converter.new.convert(ansi, state)
end
class Converter
def on_0(s) reset() end
def on_1(s) enable(STYLE_SWITCHES[:bold]) end
def on_3(s) enable(STYLE_SWITCHES[:italic]) end
def on_4(s) enable(STYLE_SWITCHES[:underline]) end
def on_8(s) enable(STYLE_SWITCHES[:conceal]) end
def on_9(s) enable(STYLE_SWITCHES[:cross]) end
def on_21(s) disable(STYLE_SWITCHES[:bold]) end
def on_22(s) disable(STYLE_SWITCHES[:bold]) end
def on_23(s) disable(STYLE_SWITCHES[:italic]) end
def on_24(s) disable(STYLE_SWITCHES[:underline]) end
def on_28(s) disable(STYLE_SWITCHES[:conceal]) end
def on_29(s) disable(STYLE_SWITCHES[:cross]) end
def on_30(s) set_fg_color(0) end
def on_31(s) set_fg_color(1) end
def on_32(s) set_fg_color(2) end
def on_33(s) set_fg_color(3) end
def on_34(s) set_fg_color(4) end
def on_35(s) set_fg_color(5) end
def on_36(s) set_fg_color(6) end
def on_37(s) set_fg_color(7) end
def on_38(s) set_fg_color_256(s) end
def on_39(s) set_fg_color(9) end
def on_40(s) set_bg_color(0) end
def on_41(s) set_bg_color(1) end
def on_42(s) set_bg_color(2) end
def on_43(s) set_bg_color(3) end
def on_44(s) set_bg_color(4) end
def on_45(s) set_bg_color(5) end
def on_46(s) set_bg_color(6) end
def on_47(s) set_bg_color(7) end
def on_48(s) set_bg_color_256(s) end
def on_49(s) set_bg_color(9) end
def on_90(s) set_fg_color(0, 'l') end
def on_91(s) set_fg_color(1, 'l') end
def on_92(s) set_fg_color(2, 'l') end
def on_93(s) set_fg_color(3, 'l') end
def on_94(s) set_fg_color(4, 'l') end
def on_95(s) set_fg_color(5, 'l') end
def on_96(s) set_fg_color(6, 'l') end
def on_97(s) set_fg_color(7, 'l') end
def on_99(s) set_fg_color(9, 'l') end
def on_100(s) set_bg_color(0, 'l') end
def on_101(s) set_bg_color(1, 'l') end
def on_102(s) set_bg_color(2, 'l') end
def on_103(s) set_bg_color(3, 'l') end
def on_104(s) set_bg_color(4, 'l') end
def on_105(s) set_bg_color(5, 'l') end
def on_106(s) set_bg_color(6, 'l') end
def on_107(s) set_bg_color(7, 'l') end
def on_109(s) set_bg_color(9, 'l') end
attr_accessor :offset, :n_open_tags, :fg_color, :bg_color, :style_mask
STATE_PARAMS = [:offset, :n_open_tags, :fg_color, :bg_color, :style_mask].freeze
def convert(stream, new_state)
reset_state
restore_state(new_state, stream) if new_state.present?
append = false
truncated = false
cur_offset = stream.tell
if cur_offset > @offset
@offset = cur_offset
truncated = true
else
stream.seek(@offset)
append = @offset > 0
end
start_offset = @offset
open_new_tag
stream.each_line do |line|
s = StringScanner.new(line)
until s.eos?
if s.scan(/\e([@-_])(.*?)([@-~])/)
handle_sequence(s)
elsif s.scan(/\e(([@-_])(.*?)?)?$/)
break
elsif s.scan(/</)
@out << '&lt;'
elsif s.scan(/\r?\n/)
@out << '<br>'
else
@out << s.scan(/./m)
end
@offset += s.matched_size
end
end
close_open_tags()
OpenStruct.new(
html: @out.force_encoding(Encoding.default_external),
state: state,
append: append,
truncated: truncated,
offset: start_offset,
size: stream.tell - start_offset,
total: stream.size
)
end
def handle_sequence(s)
indicator = s[1]
commands = s[2].split ';'
terminator = s[3]
# We are only interested in color and text style changes - triggered by
# sequences starting with '\e[' and ending with 'm'. Any other control
# sequence gets stripped (including stuff like "delete last line")
return unless indicator == '[' && terminator == 'm'
close_open_tags()
if commands.empty?()
reset()
return
end
evaluate_command_stack(commands)
open_new_tag
end
def evaluate_command_stack(stack)
return unless command = stack.shift()
if self.respond_to?("on_#{command}", true)
self.__send__("on_#{command}", stack) # rubocop:disable GitlabSecurity/PublicSend
end
evaluate_command_stack(stack)
end
def open_new_tag
css_classes = []
unless @fg_color.nil?
fg_color = @fg_color
# Most terminals show bold colored text in the light color variant
# Let's mimic that here
if @style_mask & STYLE_SWITCHES[:bold] != 0
fg_color.sub!(/fg-(\w{2,}+)/, 'fg-l-\1')
end
css_classes << fg_color
end
css_classes << @bg_color unless @bg_color.nil?
STYLE_SWITCHES.each do |css_class, flag|
css_classes << "term-#{css_class}" if @style_mask & flag != 0
end
return if css_classes.empty?
@out << %{<span class="#{css_classes.join(' ')}">}
@n_open_tags += 1
end
def close_open_tags
while @n_open_tags > 0
@out << %{</span>}
@n_open_tags -= 1
end
end
def reset_state
@offset = 0
@n_open_tags = 0
@out = ''
reset
end
def state
state = STATE_PARAMS.inject({}) do |h, param|
h[param] = send(param) # rubocop:disable GitlabSecurity/PublicSend
h
end
Base64.urlsafe_encode64(state.to_json)
end
def restore_state(new_state, stream)
state = Base64.urlsafe_decode64(new_state)
state = JSON.parse(state, symbolize_names: true)
return if state[:offset].to_i > stream.size
STATE_PARAMS.each do |param|
send("#{param}=".to_sym, state[param]) # rubocop:disable GitlabSecurity/PublicSend
end
end
def reset
@fg_color = nil
@bg_color = nil
@style_mask = 0
end
def enable(flag)
@style_mask |= flag
end
def disable(flag)
@style_mask &= ~flag
end
def set_fg_color(color_index, prefix = nil)
@fg_color = get_term_color_class(color_index, ["fg", prefix])
end
def set_bg_color(color_index, prefix = nil)
@bg_color = get_term_color_class(color_index, ["bg", prefix])
end
def get_term_color_class(color_index, prefix)
color_name = COLOR[color_index]
return nil if color_name.nil?
get_color_class(["term", prefix, color_name])
end
def set_fg_color_256(command_stack)
css_class = get_xterm_color_class(command_stack, "fg")
@fg_color = css_class unless css_class.nil?
end
def set_bg_color_256(command_stack)
css_class = get_xterm_color_class(command_stack, "bg")
@bg_color = css_class unless css_class.nil?
end
def get_xterm_color_class(command_stack, prefix)
# the 38 and 48 commands have to be followed by "5" and the color index
return unless command_stack.length >= 2
return unless command_stack[0] == "5"
command_stack.shift() # ignore the "5" command
color_index = command_stack.shift().to_i
return unless color_index >= 0
return unless color_index <= 255
get_color_class(["xterm", prefix, color_index])
end
def get_color_class(segments)
[segments].flatten.compact.join('-')
end
end
end
end
end
module Gitlab
module Ci
module Charts
module DailyInterval
def grouped_count(query)
query
.group("DATE(#{::Ci::Pipeline.table_name}.created_at)")
.count(:created_at)
.transform_keys { |date| date.strftime(@format) }
end
def interval_step
@interval_step ||= 1.day
end
end
module MonthlyInterval
def grouped_count(query)
if Gitlab::Database.postgresql?
query
.group("to_char(#{::Ci::Pipeline.table_name}.created_at, '01 Month YYYY')")
.count(:created_at)
.transform_keys(&:squish)
else
query
.group("DATE_FORMAT(#{::Ci::Pipeline.table_name}.created_at, '01 %M %Y')")
.count(:created_at)
end
end
def interval_step
@interval_step ||= 1.month
end
end
class Chart
attr_reader :labels, :total, :success, :project, :pipeline_times
def initialize(project)
@labels = []
@total = []
@success = []
@pipeline_times = []
@project = project
collect
end
def collect
query = project.pipelines
.where("? > #{::Ci::Pipeline.table_name}.created_at AND #{::Ci::Pipeline.table_name}.created_at > ?", @to, @from) # rubocop:disable GitlabSecurity/SqlInjection
totals_count = grouped_count(query)
success_count = grouped_count(query.success)
current = @from
while current < @to
label = current.strftime(@format)
@labels << label
@total << (totals_count[label] || 0)
@success << (success_count[label] || 0)
current += interval_step
end
end
end
class YearChart < Chart
include MonthlyInterval
def initialize(*)
@to = Date.today.end_of_month
@from = @to.years_ago(1).beginning_of_month
@format = '%d %B %Y'
super
end
end
class MonthChart < Chart
include DailyInterval
def initialize(*)
@to = Date.today
@from = @to - 30.days
@format = '%d %B'
super
end
end
class WeekChart < Chart
include DailyInterval
def initialize(*)
@to = Date.today
@from = @to - 7.days
@format = '%d %B'
super
end
end
class PipelineTime < Chart
def collect
commits = project.pipelines.last(30)
commits.each do |commit|
@labels << commit.short_sha
duration = commit.duration || 0
@pipeline_times << (duration / 60)
end
end
end
end
end
end
module Gitlab
module Ci::MaskSecret
class << self
def mask!(value, token)
return value unless value.present? && token.present?
value.gsub!(token, 'x' * token.length)
value
end
end
end
end
module Gitlab
module Ci
module Model
def table_name_prefix
"ci_"
end
def model_name
@model_name ||= ActiveModel::Name.new(self, nil, self.name.split("::").last)
end
end
end
end
......@@ -56,13 +56,13 @@ module Gitlab
end
def html_with_state(state = nil)
::Ci::Ansi2html.convert(stream, state)
::Gitlab::Ci::Ansi2html.convert(stream, state)
end
def html(last_lines: nil)
text = raw(last_lines: last_lines)
buffer = StringIO.new(text)
::Ci::Ansi2html.convert(buffer).html
::Gitlab::Ci::Ansi2html.convert(buffer).html
end
def extract_coverage(regex)
......
module Gitlab
module Ci
class YamlProcessor
ValidationError = Class.new(StandardError)
include Gitlab::Ci::Config::Entry::LegacyValidationHelpers
attr_reader :path, :cache, :stages, :jobs
def initialize(config, path = nil)
@ci_config = Gitlab::Ci::Config.new(config)
@config = @ci_config.to_hash
@path = path
unless @ci_config.valid?
raise ValidationError, @ci_config.errors.first
end
initial_parsing
rescue Gitlab::Ci::Config::Loader::FormatError => e
raise ValidationError, e.message
end
def builds_for_stage_and_ref(stage, ref, tag = false, source = nil)
jobs_for_stage_and_ref(stage, ref, tag, source).map do |name, _|
build_attributes(name)
end
end
def builds
@jobs.map do |name, _|
build_attributes(name)
end
end
def stage_seeds(pipeline)
seeds = @stages.uniq.map do |stage|
builds = pipeline_stage_builds(stage, pipeline)
Gitlab::Ci::Stage::Seed.new(pipeline, stage, builds) if builds.any?
end
seeds.compact
end
def build_attributes(name)
job = @jobs[name.to_sym] || {}
{ stage_idx: @stages.index(job[:stage]),
stage: job[:stage],
commands: job[:commands],
tag_list: job[:tags] || [],
name: job[:name].to_s,
allow_failure: job[:ignore],
when: job[:when] || 'on_success',
environment: job[:environment_name],
coverage_regex: job[:coverage],
yaml_variables: yaml_variables(name),
options: {
image: job[:image],
services: job[:services],
artifacts: job[:artifacts],
cache: job[:cache],
dependencies: job[:dependencies],
before_script: job[:before_script],
script: job[:script],
after_script: job[:after_script],
environment: job[:environment],
retry: job[:retry]
}.compact }
end
def self.validation_message(content)
return 'Please provide content of .gitlab-ci.yml' if content.blank?
begin
Gitlab::Ci::YamlProcessor.new(content)
nil
rescue ValidationError, Psych::SyntaxError => e
e.message
end
end
private
def pipeline_stage_builds(stage, pipeline)
builds = builds_for_stage_and_ref(
stage, pipeline.ref, pipeline.tag?, pipeline.source)
builds.select do |build|
job = @jobs[build.fetch(:name).to_sym]
has_kubernetes = pipeline.has_kubernetes_active?
only_kubernetes = job.dig(:only, :kubernetes)
except_kubernetes = job.dig(:except, :kubernetes)
[!only_kubernetes && !except_kubernetes,
only_kubernetes && has_kubernetes,
except_kubernetes && !has_kubernetes].any?
end
end
def jobs_for_ref(ref, tag = false, source = nil)
@jobs.select do |_, job|
process?(job.dig(:only, :refs), job.dig(:except, :refs), ref, tag, source)
end
end
def jobs_for_stage_and_ref(stage, ref, tag = false, source = nil)
jobs_for_ref(ref, tag, source).select do |_, job|
job[:stage] == stage
end
end
def initial_parsing
##
# Global config
#
@before_script = @ci_config.before_script
@image = @ci_config.image
@after_script = @ci_config.after_script
@services = @ci_config.services
@variables = @ci_config.variables
@stages = @ci_config.stages
@cache = @ci_config.cache
##
# Jobs
#
@jobs = @ci_config.jobs
@jobs.each do |name, job|
# logical validation for job
validate_job_stage!(name, job)
validate_job_dependencies!(name, job)
validate_job_environment!(name, job)
end
end
def yaml_variables(name)
variables = (@variables || {})
.merge(job_variables(name))
variables.map do |key, value|
{ key: key.to_s, value: value, public: true }
end
end
def job_variables(name)
job = @jobs[name.to_sym]
return {} unless job
job[:variables] || {}
end
def validate_job_stage!(name, job)
return unless job[:stage]
unless job[:stage].is_a?(String) && job[:stage].in?(@stages)
raise ValidationError, "#{name} job: stage parameter should be #{@stages.join(", ")}"
end
end
def validate_job_dependencies!(name, job)
return unless job[:dependencies]
stage_index = @stages.index(job[:stage])
job[:dependencies].each do |dependency|
raise ValidationError, "#{name} job: undefined dependency: #{dependency}" unless @jobs[dependency.to_sym]
unless @stages.index(@jobs[dependency.to_sym][:stage]) < stage_index
raise ValidationError, "#{name} job: dependency #{dependency} is not defined in prior stages"
end
end
end
def validate_job_environment!(name, job)
return unless job[:environment]
return unless job[:environment].is_a?(Hash)
environment = job[:environment]
validate_on_stop_job!(name, environment, environment[:on_stop])
end
def validate_on_stop_job!(name, environment, on_stop)
return unless on_stop
on_stop_job = @jobs[on_stop.to_sym]
unless on_stop_job
raise ValidationError, "#{name} job: on_stop job #{on_stop} is not defined"
end
unless on_stop_job[:environment]
raise ValidationError, "#{name} job: on_stop job #{on_stop} does not have environment defined"
end
unless on_stop_job[:environment][:name] == environment[:name]
raise ValidationError, "#{name} job: on_stop job #{on_stop} have different environment name"
end
unless on_stop_job[:environment][:action] == 'stop'
raise ValidationError, "#{name} job: on_stop job #{on_stop} needs to have action stop defined"
end
end
def process?(only_params, except_params, ref, tag, source)
if only_params.present?
return false unless matching?(only_params, ref, tag, source)
end
if except_params.present?
return false if matching?(except_params, ref, tag, source)
end
true
end
def matching?(patterns, ref, tag, source)
patterns.any? do |pattern|
pattern, path = pattern.split('@', 2)
matches_path?(path) && matches_pattern?(pattern, ref, tag, source)
end
end
def matches_path?(path)
return true unless path
path == self.path
end
def matches_pattern?(pattern, ref, tag, source)
return true if tag && pattern == 'tags'
return true if !tag && pattern == 'branches'
return true if source_to_pattern(source) == pattern
if pattern.first == "/" && pattern.last == "/"
Regexp.new(pattern[1...-1]) =~ ref
else
pattern == ref
end
end
def source_to_pattern(source)
if %w[api external web].include?(source)
source
else
source&.pluralize
end
end
end
end
end
require 'spec_helper'
module Ci
describe GitlabCiYamlProcessor, :lib do
subject { described_class.new(config, path) }
let(:path) { 'path' }
describe 'our current .gitlab-ci.yml' do
let(:config) { File.read("#{Rails.root}/.gitlab-ci.yml") }
it 'is valid' do
error_message = described_class.validation_message(config)
expect(error_message).to be_nil
end
end
describe '#build_attributes' do
subject { described_class.new(config, path).build_attributes(:rspec) }
describe 'coverage entry' do
describe 'code coverage regexp' do
let(:config) do
YAML.dump(rspec: { script: 'rspec',
coverage: '/Code coverage: \d+\.\d+/' })
end
it 'includes coverage regexp in build attributes' do
expect(subject)
.to include(coverage_regex: 'Code coverage: \d+\.\d+')
end
end
end
describe 'retry entry' do
context 'when retry count is specified' do
let(:config) do
YAML.dump(rspec: { script: 'rspec', retry: 1 })
end
it 'includes retry count in build options attribute' do
expect(subject[:options]).to include(retry: 1)
end
end
context 'when retry count is not specified' do
let(:config) do
YAML.dump(rspec: { script: 'rspec' })
end
it 'does not persist retry count in the database' do
expect(subject[:options]).not_to have_key(:retry)
end
end
end
describe 'allow failure entry' do
context 'when job is a manual action' do
context 'when allow_failure is defined' do
let(:config) do
YAML.dump(rspec: { script: 'rspec',
when: 'manual',
allow_failure: false })
end
it 'is not allowed to fail' do
expect(subject[:allow_failure]).to be false
end
end
context 'when allow_failure is not defined' do
let(:config) do
YAML.dump(rspec: { script: 'rspec',
when: 'manual' })
end
it 'is allowed to fail' do
expect(subject[:allow_failure]).to be true
end
end
end
context 'when job is not a manual action' do
context 'when allow_failure is defined' do
let(:config) do
YAML.dump(rspec: { script: 'rspec',
allow_failure: false })
end
it 'is not allowed to fail' do
expect(subject[:allow_failure]).to be false
end
end
context 'when allow_failure is not defined' do
let(:config) do
YAML.dump(rspec: { script: 'rspec' })
end
it 'is not allowed to fail' do
expect(subject[:allow_failure]).to be false
end
end
end
end
end
describe '#stage_seeds' do
context 'when no refs policy is specified' do
let(:config) do
YAML.dump(production: { stage: 'deploy', script: 'cap prod' },
rspec: { stage: 'test', script: 'rspec' },
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
context 'when kubernetes policy is specified' do
let(:pipeline) { create(:ci_empty_pipeline) }
let(:config) do
YAML.dump(
spinach: { stage: 'test', script: 'spinach' },
production: {
stage: 'deploy',
script: 'cap',
only: { kubernetes: 'active' }
}
)
end
context 'when kubernetes is active' do
let(:project) { create(:kubernetes_project) }
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
it 'returns seeds for kubernetes dependent job' do
seeds = subject.stage_seeds(pipeline)
expect(seeds.size).to eq 2
expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
expect(seeds.second.builds.dig(0, :name)).to eq 'production'
end
end
context 'when kubernetes is not active' do
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
describe "#builds_for_stage_and_ref" do
let(:type) { 'test' }
it "returns builds if no branch specified" do
config = YAML.dump({
before_script: ["pwd"],
rspec: { script: "rspec" }
})
config_processor = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref(type, "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 = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref(type, "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 = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref(type, "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 = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref(type, "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 = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref(type, "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 = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref(type, "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 = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
end
it "returns builds if only 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, only: [possibility[:keyword]] }
})
config_processor = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(1)
end
end
it "does not return builds if only has special keywords specified and source doesn't match" do
possibilities = [{ keyword: 'pushes', source: 'web' },
{ 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 = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(0)
end
end
it "returns builds if only has current repository path" do
config = YAML.dump({
before_script: ["pwd"],
rspec: { script: "rspec", type: type, only: ["branches@path"] }
})
config_processor = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
end
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 = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref(type, "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 = GitlabCiYamlProcessor.new(config, 'fork')
expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2)
expect(config_processor.builds_for_stage_and_ref("test", "deploy").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(1)
end
context 'for invalid value' do
let(:config) { { rspec: { script: "rspec", type: "test", only: only } } }
let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) }
context 'when it is integer' do
let(:only) { 1 }
it do
expect { processor }.to raise_error(GitlabCiYamlProcessor::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(GitlabCiYamlProcessor::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(GitlabCiYamlProcessor::ValidationError,
'jobs:rspec:only config should be an array of strings or regexps')
end
end
end
end
describe 'except' do
it "returns builds if except has another branch" do
config = YAML.dump({
before_script: ["pwd"],
rspec: { script: "rspec", except: ["deploy"] }
})
config_processor = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref(type, "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 = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
end
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 = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref(type, "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 = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
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 = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref(type, "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 = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref(type, "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 = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(0)
end
end
it "returns builds if except has special keywords specified and source doesn't match" do
possibilities = [{ keyword: 'pushes', source: 'web' },
{ 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 = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(1)
end
end
it "does not return builds if except has current repository path" do
config = YAML.dump({
before_script: ["pwd"],
rspec: { script: "rspec", type: type, except: ["branches@path"] }
})
config_processor = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref(type, "deploy").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 = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
end
it "returns build except specified type" do
config = YAML.dump({
before_script: ["pwd"],
rspec: { script: "rspec", type: "test", except: ["master", "deploy", "test@fork"] },
staging: { script: "deploy", type: "deploy", except: ["master"] },
production: { script: "deploy", type: "deploy", except: ["master@fork"] }
})
config_processor = GitlabCiYamlProcessor.new(config, 'fork')
expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2)
expect(config_processor.builds_for_stage_and_ref("test", "test").size).to eq(0)
expect(config_processor.builds_for_stage_and_ref("deploy", "master").size).to eq(0)
end
context 'for invalid value' do
let(:config) { { rspec: { script: "rspec", except: except } } }
let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) }
context 'when it is integer' do
let(:except) { 1 }
it do
expect { processor }.to raise_error(GitlabCiYamlProcessor::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(GitlabCiYamlProcessor::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(GitlabCiYamlProcessor::ValidationError,
'jobs:rspec:except config should be an array of strings or regexps')
end
end
end
end
end
describe "Scripts handling" do
let(:config_data) { YAML.dump(config) }
let(:config_processor) { GitlabCiYamlProcessor.new(config_data, path) }
subject { config_processor.builds_for_stage_and_ref("test", "master").first }
describe "before_script" do
context "in global context" do
let(:config) do
{
before_script: ["global script"],
test: { script: ["script"] }
}
end
it "return commands with scripts concencaced" do
expect(subject[:commands]).to eq("global script\nscript")
end
end
context "overwritten in local context" do
let(:config) do
{
before_script: ["global script"],
test: { before_script: ["local script"], script: ["script"] }
}
end
it "return commands with scripts concencaced" do
expect(subject[:commands]).to eq("local script\nscript")
end
end
end
describe "script" do
let(:config) do
{
test: { script: ["script"] }
}
end
it "return commands with scripts concencaced" do
expect(subject[:commands]).to eq("script")
end
end
describe "after_script" do
context "in global context" do
let(:config) do
{
after_script: ["after_script"],
test: { script: ["script"] }
}
end
it "return after_script in options" do
expect(subject[:options][:after_script]).to eq(["after_script"])
end
end
context "overwritten in local context" do
let(:config) do
{
after_script: ["local after_script"],
test: { after_script: ["local after_script"], script: ["script"] }
}
end
it "return after_script in options" do
expect(subject[:options][:after_script]).to eq(["local after_script"])
end
end
end
end
describe "Image and service handling" do
context "when extended docker configuration is used" do
it "returns image and service when defined" do
config = YAML.dump({ image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] },
services: ["mysql", { name: "docker:dind", alias: "docker",
entrypoint: ["/usr/local/bin/init", "run"],
command: ["/usr/local/bin/init", "run"] }],
before_script: ["pwd"],
rspec: { script: "rspec" } })
config_processor = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref("test", "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"],
image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] },
services: [{ name: "mysql" },
{ name: "docker:dind", alias: "docker", entrypoint: ["/usr/local/bin/init", "run"],
command: ["/usr/local/bin/init", "run"] }]
},
allow_failure: false,
when: "on_success",
environment: nil,
yaml_variables: []
})
end
it "returns image and service when overridden for job" do
config = YAML.dump({ image: "ruby:2.1",
services: ["mysql"],
before_script: ["pwd"],
rspec: { image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] },
services: [{ name: "postgresql", alias: "db-pg",
entrypoint: ["/usr/local/bin/init", "run"],
command: ["/usr/local/bin/init", "run"] }, "docker:dind"],
script: "rspec" } })
config_processor = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref("test", "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"],
image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] },
services: [{ name: "postgresql", alias: "db-pg", entrypoint: ["/usr/local/bin/init", "run"],
command: ["/usr/local/bin/init", "run"] },
{ name: "docker:dind" }]
},
allow_failure: false,
when: "on_success",
environment: nil,
yaml_variables: []
})
end
end
context "when etended docker configuration is not used" do
it "returns image and service when defined" do
config = YAML.dump({ image: "ruby:2.1",
services: ["mysql", "docker:dind"],
before_script: ["pwd"],
rspec: { script: "rspec" } })
config_processor = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref("test", "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"],
image: { name: "ruby:2.1" },
services: [{ name: "mysql" }, { name: "docker:dind" }]
},
allow_failure: false,
when: "on_success",
environment: nil,
yaml_variables: []
})
end
it "returns image and service when overridden for job" do
config = YAML.dump({ image: "ruby:2.1",
services: ["mysql"],
before_script: ["pwd"],
rspec: { image: "ruby:2.5", services: ["postgresql", "docker:dind"], script: "rspec" } })
config_processor = GitlabCiYamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref("test", "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"],
image: { name: "ruby:2.5" },
services: [{ name: "postgresql" }, { name: "docker:dind" }]
},
allow_failure: false,
when: "on_success",
environment: nil,
yaml_variables: []
})
end
end
end
describe 'Variables' do
let(:config_processor) { GitlabCiYamlProcessor.new(YAML.dump(config), path) }
subject { config_processor.builds.first[:yaml_variables] }
context 'when global variables are defined' do
let(:variables) do
{ 'VAR1' => 'value1', 'VAR2' => 'value2' }
end
let(:config) do
{
variables: variables,
before_script: ['pwd'],
rspec: { script: 'rspec' }
}
end
it 'returns global variables' do
expect(subject).to contain_exactly(
{ key: 'VAR1', value: 'value1', public: true },
{ key: 'VAR2', value: 'value2', public: true }
)
end
end
context 'when job and global variables are defined' do
let(:global_variables) do
{ 'VAR1' => 'global1', 'VAR3' => 'global3' }
end
let(:job_variables) do
{ 'VAR1' => 'value1', 'VAR2' => 'value2' }
end
let(:config) do
{
before_script: ['pwd'],
variables: global_variables,
rspec: { script: 'rspec', variables: job_variables }
}
end
it 'returns all unique variables' do
expect(subject).to contain_exactly(
{ key: 'VAR3', value: 'global3', public: true },
{ key: 'VAR1', value: 'value1', public: true },
{ key: 'VAR2', value: 'value2', public: true }
)
end
end
context 'when job variables are defined' do
let(:config) do
{
before_script: ['pwd'],
rspec: { script: 'rspec', variables: variables }
}
end
context 'when syntax is correct' do
let(:variables) do
{ 'VAR1' => 'value1', 'VAR2' => 'value2' }
end
it 'returns job variables' do
expect(subject).to contain_exactly(
{ key: 'VAR1', value: 'value1', public: true },
{ key: 'VAR2', value: 'value2', public: true }
)
end
end
context 'when syntax is incorrect' do
context 'when variables defined but invalid' do
let(:variables) do
%w(VAR1 value1 VAR2 value2)
end
it 'raises error' do
expect { subject }
.to raise_error(GitlabCiYamlProcessor::ValidationError,
/jobs:rspec:variables config should be a hash of key value pairs/)
end
end
context 'when variables key defined but value not specified' do
let(:variables) do
nil
end
it 'returns empty array' do
##
# When variables config is empty, we assume this is a valid
# configuration, see issue #18775
#
expect(subject).to be_an_instance_of(Array)
expect(subject).to be_empty
end
end
end
end
context 'when job variables are not defined' do
let(:config) do
{
before_script: ['pwd'],
rspec: { script: 'rspec' }
}
end
it 'returns empty array' do
expect(subject).to be_an_instance_of(Array)
expect(subject).to be_empty
end
end
end
describe "When" do
%w(on_success on_failure always).each do |when_state|
it "returns #{when_state} when defined" do
config = YAML.dump({
rspec: { script: "rspec", when: when_state }
})
config_processor = GitlabCiYamlProcessor.new(config, path)
builds = config_processor.builds_for_stage_and_ref("test", "master")
expect(builds.size).to eq(1)
expect(builds.first[:when]).to eq(when_state)
end
end
end
describe 'cache' do
context 'when cache definition has unknown keys' do
it 'raises relevant validation error' do
config = YAML.dump(
{ cache: { untracked: true, invalid: 'key' },
rspec: { script: 'rspec' } })
expect { GitlabCiYamlProcessor.new(config) }.to raise_error(
GitlabCiYamlProcessor::ValidationError,
'cache config contains unknown keys: invalid'
)
end
end
it "returns cache when defined globally" do
config = YAML.dump({
cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' },
rspec: {
script: "rspec"
}
})
config_processor = GitlabCiYamlProcessor.new(config)
expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
paths: ["logs/", "binaries/"],
untracked: true,
key: 'key',
policy: 'pull-push'
)
end
it "returns cache when defined in a job" do
config = YAML.dump({
rspec: {
cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' },
script: "rspec"
}
})
config_processor = GitlabCiYamlProcessor.new(config)
expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
paths: ["logs/", "binaries/"],
untracked: true,
key: 'key',
policy: 'pull-push'
)
end
it "overwrite cache when defined for a job and globally" do
config = YAML.dump({
cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'global' },
rspec: {
script: "rspec",
cache: { paths: ["test/"], untracked: false, key: 'local' }
}
})
config_processor = GitlabCiYamlProcessor.new(config)
expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
paths: ["test/"],
untracked: false,
key: 'local',
policy: 'pull-push'
)
end
end
describe "Artifacts" do
it "returns artifacts when defined" do
config = YAML.dump({
image: "ruby:2.1",
services: ["mysql"],
before_script: ["pwd"],
rspec: {
artifacts: {
paths: ["logs/", "binaries/"],
untracked: true,
name: "custom_name",
expire_in: "7d"
},
script: "rspec"
}
})
config_processor = GitlabCiYamlProcessor.new(config)
expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref("test", "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"],
image: { name: "ruby:2.1" },
services: [{ name: "mysql" }],
artifacts: {
name: "custom_name",
paths: ["logs/", "binaries/"],
untracked: true,
expire_in: "7d"
}
},
when: "on_success",
allow_failure: false,
environment: nil,
yaml_variables: []
})
end
%w[on_success on_failure always].each do |when_state|
it "returns artifacts for when #{when_state} defined" do
config = YAML.dump({
rspec: {
script: "rspec",
artifacts: { paths: ["logs/", "binaries/"], when: when_state }
}
})
config_processor = GitlabCiYamlProcessor.new(config, path)
builds = config_processor.builds_for_stage_and_ref("test", "master")
expect(builds.size).to eq(1)
expect(builds.first[:options][:artifacts][:when]).to eq(when_state)
end
end
end
describe '#environment' do
let(:config) do
{
deploy_to_production: { stage: 'deploy', script: 'test', environment: environment }
}
end
let(:processor) { GitlabCiYamlProcessor.new(YAML.dump(config)) }
let(:builds) { processor.builds_for_stage_and_ref('deploy', 'master') }
context 'when a production environment is specified' do
let(:environment) { 'production' }
it 'does return production' do
expect(builds.size).to eq(1)
expect(builds.first[:environment]).to eq(environment)
expect(builds.first[:options]).to include(environment: { name: environment, action: "start" })
end
end
context 'when hash is specified' do
let(:environment) do
{ name: 'production',
url: 'http://production.gitlab.com' }
end
it 'does return production and URL' do
expect(builds.size).to eq(1)
expect(builds.first[:environment]).to eq(environment[:name])
expect(builds.first[:options]).to include(environment: environment)
end
context 'the url has a port as variable' do
let(:environment) do
{ name: 'production',
url: 'http://production.gitlab.com:$PORT' }
end
it 'allows a variable for the port' do
expect(builds.size).to eq(1)
expect(builds.first[:environment]).to eq(environment[:name])
expect(builds.first[:options]).to include(environment: environment)
end
end
end
context 'when no environment is specified' do
let(:environment) { nil }
it 'does return nil environment' do
expect(builds.size).to eq(1)
expect(builds.first[:environment]).to be_nil
end
end
context 'is not a string' do
let(:environment) { 1 }
it 'raises error' do
expect { builds }.to raise_error(
'jobs:deploy_to_production:environment config should be a hash or a string')
end
end
context 'is not a valid string' do
let(:environment) { 'production:staging' }
it 'raises error' do
expect { builds }.to raise_error("jobs:deploy_to_production:environment name #{Gitlab::Regex.environment_name_regex_message}")
end
end
context 'when on_stop is specified' do
let(:review) { { stage: 'deploy', script: 'test', environment: { name: 'review', on_stop: 'close_review' } } }
let(:config) { { review: review, close_review: close_review }.compact }
context 'with matching job' do
let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review', action: 'stop' } } }
it 'does return a list of builds' do
expect(builds.size).to eq(2)
expect(builds.first[:environment]).to eq('review')
end
end
context 'without matching job' do
let(:close_review) { nil }
it 'raises error' do
expect { builds }.to raise_error('review job: on_stop job close_review is not defined')
end
end
context 'with close job without environment' do
let(:close_review) { { stage: 'deploy', script: 'test' } }
it 'raises error' do
expect { builds }.to raise_error('review job: on_stop job close_review does not have environment defined')
end
end
context 'with close job for different environment' do
let(:close_review) { { stage: 'deploy', script: 'test', environment: 'production' } }
it 'raises error' do
expect { builds }.to raise_error('review job: on_stop job close_review have different environment name')
end
end
context 'with close job without stop action' do
let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review' } } }
it 'raises error' do
expect { builds }.to raise_error('review job: on_stop job close_review needs to have action stop defined')
end
end
end
end
describe "Dependencies" do
let(:config) do
{
build1: { stage: 'build', script: 'test' },
build2: { stage: 'build', script: 'test' },
test1: { stage: 'test', script: 'test', dependencies: dependencies },
test2: { stage: 'test', script: 'test' },
deploy: { stage: 'test', script: 'test' }
}
end
subject { GitlabCiYamlProcessor.new(YAML.dump(config)) }
context 'no dependencies' do
let(:dependencies) { }
it { expect { subject }.not_to raise_error }
end
context 'dependencies to builds' do
let(:dependencies) { %w(build1 build2) }
it { expect { subject }.not_to raise_error }
end
context 'dependencies to builds defined as symbols' do
let(:dependencies) { [:build1, :build2] }
it { expect { subject }.not_to raise_error }
end
context 'undefined dependency' do
let(:dependencies) { ['undefined'] }
it { expect { subject }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'test1 job: undefined dependency: undefined') }
end
context 'dependencies to deploy' do
let(:dependencies) { ['deploy'] }
it { expect { subject }.to raise_error(GitlabCiYamlProcessor::ValidationError, 'test1 job: dependency deploy is not defined in prior stages') }
end
end
describe "Hidden jobs" do
let(:config_processor) { GitlabCiYamlProcessor.new(config) }
subject { config_processor.builds_for_stage_and_ref("test", "master") }
shared_examples 'hidden_job_handling' do
it "doesn't create jobs that start with dot" do
expect(subject.size).to eq(1)
expect(subject.first).to eq({
stage: "test",
stage_idx: 1,
name: "normal_job",
commands: "test",
coverage_regex: nil,
tag_list: [],
options: {
script: ["test"]
},
when: "on_success",
allow_failure: false,
environment: nil,
yaml_variables: []
})
end
end
context 'when hidden job have a script definition' do
let(:config) do
YAML.dump({
'.hidden_job' => { image: 'ruby:2.1', script: 'test' },
'normal_job' => { script: 'test' }
})
end
it_behaves_like 'hidden_job_handling'
end
context "when hidden job doesn't have a script definition" do
let(:config) do
YAML.dump({
'.hidden_job' => { image: 'ruby:2.1' },
'normal_job' => { script: 'test' }
})
end
it_behaves_like 'hidden_job_handling'
end
end
describe "YAML Alias/Anchor" do
let(:config_processor) { GitlabCiYamlProcessor.new(config) }
subject { config_processor.builds_for_stage_and_ref("build", "master") }
shared_examples 'job_templates_handling' do
it "is correctly supported for jobs" do
expect(subject.size).to eq(2)
expect(subject.first).to eq({
stage: "build",
stage_idx: 0,
name: "job1",
commands: "execute-script-for-job",
coverage_regex: nil,
tag_list: [],
options: {
script: ["execute-script-for-job"]
},
when: "on_success",
allow_failure: false,
environment: nil,
yaml_variables: []
})
expect(subject.second).to eq({
stage: "build",
stage_idx: 0,
name: "job2",
commands: "execute-script-for-job",
coverage_regex: nil,
tag_list: [],
options: {
script: ["execute-script-for-job"]
},
when: "on_success",
allow_failure: false,
environment: nil,
yaml_variables: []
})
end
end
context 'when template is a job' do
let(:config) do
<<EOT
job1: &JOBTMPL
stage: build
script: execute-script-for-job
job2: *JOBTMPL
EOT
end
it_behaves_like 'job_templates_handling'
end
context 'when template is a hidden job' do
let(:config) do
<<EOT
.template: &JOBTMPL
stage: build
script: execute-script-for-job
job1: *JOBTMPL
job2: *JOBTMPL
EOT
end
it_behaves_like 'job_templates_handling'
end
context 'when job adds its own keys to a template definition' do
let(:config) do
<<EOT
.template: &JOBTMPL
stage: build
job1:
<<: *JOBTMPL
script: execute-script-for-job
job2:
<<: *JOBTMPL
script: execute-script-for-job
EOT
end
it_behaves_like 'job_templates_handling'
end
end
describe "Error handling" do
it "fails to parse YAML" do
expect {GitlabCiYamlProcessor.new("invalid: yaml: test")}.to raise_error(Psych::SyntaxError)
end
it "indicates that object is invalid" do
expect {GitlabCiYamlProcessor.new("invalid_yaml")}.to raise_error(GitlabCiYamlProcessor::ValidationError)
end
it "returns errors if tags parameter is invalid" do
config = YAML.dump({ rspec: { script: "test", tags: "mysql" } })
expect do
GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec tags should be an array of strings")
end
it "returns errors if before_script parameter is invalid" do
config = YAML.dump({ before_script: "bundle update", rspec: { script: "test" } })
expect do
GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "before_script config should be an array of strings")
end
it "returns errors if job before_script parameter is not an array of strings" do
config = YAML.dump({ rspec: { script: "test", before_script: [10, "test"] } })
expect do
GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:before_script config should be an array of strings")
end
it "returns errors if after_script parameter is invalid" do
config = YAML.dump({ after_script: "bundle update", rspec: { script: "test" } })
expect do
GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "after_script config should be an array of strings")
end
it "returns errors if job after_script parameter is not an array of strings" do
config = YAML.dump({ rspec: { script: "test", after_script: [10, "test"] } })
expect do
GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:after_script config should be an array of strings")
end
it "returns errors if image parameter is invalid" do
config = YAML.dump({ image: ["test"], rspec: { script: "test" } })
expect do
GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "image config should be a hash or a string")
end
it "returns errors if job name is blank" do
config = YAML.dump({ '' => { script: "test" } })
expect do
GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:job name can't be blank")
end
it "returns errors if job name is non-string" do
config = YAML.dump({ 10 => { script: "test" } })
expect do
GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:10 name should be a symbol")
end
it "returns errors if job image parameter is invalid" do
config = YAML.dump({ rspec: { script: "test", image: ["test"] } })
expect do
GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:image config should be a hash or a string")
end
it "returns errors if services parameter is not an array" do
config = YAML.dump({ services: "test", rspec: { script: "test" } })
expect do
GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "services config should be a array")
end
it "returns errors if services parameter is not an array of strings" do
config = YAML.dump({ services: [10, "test"], rspec: { script: "test" } })
expect do
GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "service config should be a hash or a string")
end
it "returns errors if job services parameter is not an array" do
config = YAML.dump({ rspec: { script: "test", services: "test" } })
expect do
GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:services config should be a array")
end
it "returns errors if job services parameter is not an array of strings" do
config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } })
expect do
GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "service config should be a hash or a string")
end
it "returns error if job configuration is invalid" do
config = YAML.dump({ extra: "bundle update" })
expect do
GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:extra config should be a hash")
end
it "returns errors if services configuration is not correct" do
config = YAML.dump({ extra: { script: 'rspec', services: "test" } })
expect do
GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:extra:services config should be a array")
end
it "returns errors if there are no jobs defined" do
config = YAML.dump({ before_script: ["bundle update"] })
expect do
GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs config should contain at least one visible job")
end
it "returns errors if there are no visible jobs defined" do
config = YAML.dump({ before_script: ["bundle update"], '.hidden'.to_sym => { script: 'ls' } })
expect do
GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs config should contain at least one visible job")
end
it "returns errors if job allow_failure parameter is not an boolean" do
config = YAML.dump({ rspec: { script: "test", allow_failure: "string" } })
expect do
GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec allow failure should be a boolean value")
end
it "returns errors if job stage is not a string" do
config = YAML.dump({ rspec: { script: "test", type: 1 } })
expect do
GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:type config should be a string")
end
it "returns errors if job stage is not a pre-defined stage" do
config = YAML.dump({ rspec: { script: "test", type: "acceptance" } })
expect do
GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy")
end
it "returns errors if job stage is not a defined stage" do
config = YAML.dump({ types: %w(build test), rspec: { script: "test", type: "acceptance" } })
expect do
GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test")
end
it "returns errors if stages is not an array" do
config = YAML.dump({ stages: "test", rspec: { script: "test" } })
expect do
GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "stages config should be an array of strings")
end
it "returns errors if stages is not an array of strings" do
config = YAML.dump({ stages: [true, "test"], rspec: { script: "test" } })
expect do
GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "stages config should be an array of strings")
end
it "returns errors if variables is not a map" do
config = YAML.dump({ variables: "test", rspec: { script: "test" } })
expect do
GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables config should be a hash of key value pairs")
end
it "returns errors if variables is not a map of key-value strings" do
config = YAML.dump({ variables: { test: false }, rspec: { script: "test" } })
expect do
GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables config should be a hash of key value pairs")
end
it "returns errors if job when is not on_success, on_failure or always" do
config = YAML.dump({ rspec: { script: "test", when: 1 } })
expect do
GitlabCiYamlProcessor.new(config, path)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec when should be on_success, on_failure, always or manual")
end
it "returns errors if job artifacts:name is not an a string" do
config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { name: 1 } } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts name should be a string")
end
it "returns errors if job artifacts:when is not an a predefined value" do
config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { when: 1 } } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts when should be on_success, on_failure or always")
end
it "returns errors if job artifacts:expire_in is not an a string" do
config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: 1 } } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration")
end
it "returns errors if job artifacts:expire_in is not an a valid duration" do
config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: "7 elephants" } } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration")
end
it "returns errors if job artifacts:untracked is not an array of strings" do
config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { untracked: "string" } } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts untracked should be a boolean value")
end
it "returns errors if job artifacts:paths is not an array of strings" do
config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { paths: "string" } } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:artifacts paths should be an array of strings")
end
it "returns errors if cache:untracked is not an array of strings" do
config = YAML.dump({ cache: { untracked: "string" }, rspec: { script: "test" } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:untracked config should be a boolean value")
end
it "returns errors if cache:paths is not an array of strings" do
config = YAML.dump({ cache: { paths: "string" }, rspec: { script: "test" } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:paths config should be an array of strings")
end
it "returns errors if cache:key is not a string" do
config = YAML.dump({ cache: { key: 1 }, rspec: { script: "test" } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "cache:key config should be a string or symbol")
end
it "returns errors if job cache:key is not an a string" do
config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: 1 } } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:key config should be a string or symbol")
end
it "returns errors if job cache:untracked is not an array of strings" do
config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { untracked: "string" } } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:untracked config should be a boolean value")
end
it "returns errors if job cache:paths is not an array of strings" do
config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { paths: "string" } } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec:cache:paths config should be an array of strings")
end
it "returns errors if job dependencies is not an array of strings" do
config = YAML.dump({ types: %w(build test), rspec: { script: "test", dependencies: "string" } })
expect do
GitlabCiYamlProcessor.new(config)
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "jobs:rspec dependencies should be an array of strings")
end
end
describe "Validate configuration templates" do
templates = Dir.glob("#{Rails.root.join('vendor/gitlab-ci-yml')}/**/*.gitlab-ci.yml")
templates.each do |file|
it "does not return errors for #{file}" do
file = File.read(file)
expect { GitlabCiYamlProcessor.new(file) }.not_to raise_error
end
end
end
describe "#validation_message" do
context "when the YAML could not be parsed" do
it "returns an error about invalid configutaion" do
content = YAML.dump("invalid: yaml: test")
expect(GitlabCiYamlProcessor.validation_message(content))
.to eq "Invalid configuration format"
end
end
context "when the tags parameter is invalid" do
it "returns an error about invalid tags" do
content = YAML.dump({ rspec: { script: "test", tags: "mysql" } })
expect(GitlabCiYamlProcessor.validation_message(content))
.to eq "jobs:rspec tags should be an array of strings"
end
end
context "when YAML content is empty" do
it "returns an error about missing content" do
expect(GitlabCiYamlProcessor.validation_message(''))
.to eq "Please provide content of .gitlab-ci.yml"
end
end
context "when the YAML is valid" do
it "does not return any errors" do
content = File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
expect(GitlabCiYamlProcessor.validation_message(content)).to be_nil
end
end
end
end
end
require 'spec_helper'
describe Ci::Ansi2html do
describe Gitlab::Ci::Ansi2html do
subject { described_class }
it "prints non-ansi as-is" do
......
require 'spec_helper'
describe Ci::Charts do
describe Gitlab::Ci::Charts do
context "pipeline_times" do
let(:project) { create(:project) }
let(:chart) { Ci::Charts::PipelineTime.new(project) }
let(:chart) { Gitlab::Ci::Charts::PipelineTime.new(project) }
subject { chart.pipeline_times }
......
require 'spec_helper'
describe Ci::MaskSecret do
describe Gitlab::Ci::MaskSecret do
subject { described_class }
describe '#mask' do
......
require 'spec_helper'
module Gitlab
module Ci
describe YamlProcessor, :lib do
subject { described_class.new(config, path) }
let(:path) { 'path' }
describe 'our current .gitlab-ci.yml' do
let(:config) { File.read("#{Rails.root}/.gitlab-ci.yml") }
it 'is valid' do
error_message = described_class.validation_message(config)
expect(error_message).to be_nil
end
end
describe '#build_attributes' do
subject { described_class.new(config, path).build_attributes(:rspec) }
describe 'coverage entry' do
describe 'code coverage regexp' do
let(:config) do
YAML.dump(rspec: { script: 'rspec',
coverage: '/Code coverage: \d+\.\d+/' })
end
it 'includes coverage regexp in build attributes' do
expect(subject)
.to include(coverage_regex: 'Code coverage: \d+\.\d+')
end
end
end
describe 'retry entry' do
context 'when retry count is specified' do
let(:config) do
YAML.dump(rspec: { script: 'rspec', retry: 1 })
end
it 'includes retry count in build options attribute' do
expect(subject[:options]).to include(retry: 1)
end
end
context 'when retry count is not specified' do
let(:config) do
YAML.dump(rspec: { script: 'rspec' })
end
it 'does not persist retry count in the database' do
expect(subject[:options]).not_to have_key(:retry)
end
end
end
describe 'allow failure entry' do
context 'when job is a manual action' do
context 'when allow_failure is defined' do
let(:config) do
YAML.dump(rspec: { script: 'rspec',
when: 'manual',
allow_failure: false })
end
it 'is not allowed to fail' do
expect(subject[:allow_failure]).to be false
end
end
context 'when allow_failure is not defined' do
let(:config) do
YAML.dump(rspec: { script: 'rspec',
when: 'manual' })
end
it 'is allowed to fail' do
expect(subject[:allow_failure]).to be true
end
end
end
context 'when job is not a manual action' do
context 'when allow_failure is defined' do
let(:config) do
YAML.dump(rspec: { script: 'rspec',
allow_failure: false })
end
it 'is not allowed to fail' do
expect(subject[:allow_failure]).to be false
end
end
context 'when allow_failure is not defined' do
let(:config) do
YAML.dump(rspec: { script: 'rspec' })
end
it 'is not allowed to fail' do
expect(subject[:allow_failure]).to be false
end
end
end
end
end
describe '#stage_seeds' do
context 'when no refs policy is specified' do
let(:config) do
YAML.dump(production: { stage: 'deploy', script: 'cap prod' },
rspec: { stage: 'test', script: 'rspec' },
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
context 'when kubernetes policy is specified' do
let(:pipeline) { create(:ci_empty_pipeline) }
let(:config) do
YAML.dump(
spinach: { stage: 'test', script: 'spinach' },
production: {
stage: 'deploy',
script: 'cap',
only: { kubernetes: 'active' }
}
)
end
context 'when kubernetes is active' do
let(:project) { create(:kubernetes_project) }
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
it 'returns seeds for kubernetes dependent job' do
seeds = subject.stage_seeds(pipeline)
expect(seeds.size).to eq 2
expect(seeds.first.builds.dig(0, :name)).to eq 'spinach'
expect(seeds.second.builds.dig(0, :name)).to eq 'production'
end
end
context 'when kubernetes is not active' do
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
describe "#builds_for_stage_and_ref" do
let(:type) { 'test' }
it "returns builds if no branch specified" do
config = YAML.dump({
before_script: ["pwd"],
rspec: { script: "rspec" }
})
config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref(type, "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, path)
expect(config_processor.builds_for_stage_and_ref(type, "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, path)
expect(config_processor.builds_for_stage_and_ref(type, "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, path)
expect(config_processor.builds_for_stage_and_ref(type, "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, path)
expect(config_processor.builds_for_stage_and_ref(type, "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, path)
expect(config_processor.builds_for_stage_and_ref(type, "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, path)
expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
end
it "returns builds if only 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, only: [possibility[:keyword]] }
})
config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(1)
end
end
it "does not return builds if only has special keywords specified and source doesn't match" do
possibilities = [{ keyword: 'pushes', source: 'web' },
{ 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, path)
expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(0)
end
end
it "returns builds if only has current repository path" do
config = YAML.dump({
before_script: ["pwd"],
rspec: { script: "rspec", type: type, only: ["branches@path"] }
})
config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
end
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, path)
expect(config_processor.builds_for_stage_and_ref(type, "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, 'fork')
expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2)
expect(config_processor.builds_for_stage_and_ref("test", "deploy").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref("deploy", "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
describe 'except' do
it "returns builds if except has another branch" do
config = YAML.dump({
before_script: ["pwd"],
rspec: { script: "rspec", except: ["deploy"] }
})
config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref(type, "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, path)
expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
end
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, path)
expect(config_processor.builds_for_stage_and_ref(type, "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, path)
expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(0)
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, path)
expect(config_processor.builds_for_stage_and_ref(type, "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, path)
expect(config_processor.builds_for_stage_and_ref(type, "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, path)
expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(0)
end
end
it "returns builds if except has special keywords specified and source doesn't match" do
possibilities = [{ keyword: 'pushes', source: 'web' },
{ 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, path)
expect(config_processor.builds_for_stage_and_ref(type, "deploy", false, possibility[:source]).size).to eq(1)
end
end
it "does not return builds if except has current repository path" do
config = YAML.dump({
before_script: ["pwd"],
rspec: { script: "rspec", type: type, except: ["branches@path"] }
})
config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref(type, "deploy").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, path)
expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
end
it "returns build except specified type" do
config = YAML.dump({
before_script: ["pwd"],
rspec: { script: "rspec", type: "test", except: ["master", "deploy", "test@fork"] },
staging: { script: "deploy", type: "deploy", except: ["master"] },
production: { script: "deploy", type: "deploy", except: ["master@fork"] }
})
config_processor = Gitlab::Ci::YamlProcessor.new(config, 'fork')
expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2)
expect(config_processor.builds_for_stage_and_ref("test", "test").size).to eq(0)
expect(config_processor.builds_for_stage_and_ref("deploy", "master").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
let(:except) { 1 }
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
describe "Scripts handling" do
let(:config_data) { YAML.dump(config) }
let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config_data, path) }
subject { config_processor.builds_for_stage_and_ref("test", "master").first }
describe "before_script" do
context "in global context" do
let(:config) do
{
before_script: ["global script"],
test: { script: ["script"] }
}
end
it "return commands with scripts concencaced" do
expect(subject[:commands]).to eq("global script\nscript")
end
end
context "overwritten in local context" do
let(:config) do
{
before_script: ["global script"],
test: { before_script: ["local script"], script: ["script"] }
}
end
it "return commands with scripts concencaced" do
expect(subject[:commands]).to eq("local script\nscript")
end
end
end
describe "script" do
let(:config) do
{
test: { script: ["script"] }
}
end
it "return commands with scripts concencaced" do
expect(subject[:commands]).to eq("script")
end
end
describe "after_script" do
context "in global context" do
let(:config) do
{
after_script: ["after_script"],
test: { script: ["script"] }
}
end
it "return after_script in options" do
expect(subject[:options][:after_script]).to eq(["after_script"])
end
end
context "overwritten in local context" do
let(:config) do
{
after_script: ["local after_script"],
test: { after_script: ["local after_script"], script: ["script"] }
}
end
it "return after_script in options" do
expect(subject[:options][:after_script]).to eq(["local after_script"])
end
end
end
end
describe "Image and service handling" do
context "when extended docker configuration is used" do
it "returns image and service when defined" do
config = YAML.dump({ image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] },
services: ["mysql", { name: "docker:dind", alias: "docker",
entrypoint: ["/usr/local/bin/init", "run"],
command: ["/usr/local/bin/init", "run"] }],
before_script: ["pwd"],
rspec: { script: "rspec" } })
config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref("test", "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"],
image: { name: "ruby:2.1", entrypoint: ["/usr/local/bin/init", "run"] },
services: [{ name: "mysql" },
{ name: "docker:dind", alias: "docker", entrypoint: ["/usr/local/bin/init", "run"],
command: ["/usr/local/bin/init", "run"] }]
},
allow_failure: false,
when: "on_success",
environment: nil,
yaml_variables: []
})
end
it "returns image and service when overridden for job" do
config = YAML.dump({ image: "ruby:2.1",
services: ["mysql"],
before_script: ["pwd"],
rspec: { image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] },
services: [{ name: "postgresql", alias: "db-pg",
entrypoint: ["/usr/local/bin/init", "run"],
command: ["/usr/local/bin/init", "run"] }, "docker:dind"],
script: "rspec" } })
config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref("test", "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"],
image: { name: "ruby:2.5", entrypoint: ["/usr/local/bin/init", "run"] },
services: [{ name: "postgresql", alias: "db-pg", entrypoint: ["/usr/local/bin/init", "run"],
command: ["/usr/local/bin/init", "run"] },
{ name: "docker:dind" }]
},
allow_failure: false,
when: "on_success",
environment: nil,
yaml_variables: []
})
end
end
context "when etended docker configuration is not used" do
it "returns image and service when defined" do
config = YAML.dump({ image: "ruby:2.1",
services: ["mysql", "docker:dind"],
before_script: ["pwd"],
rspec: { script: "rspec" } })
config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref("test", "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"],
image: { name: "ruby:2.1" },
services: [{ name: "mysql" }, { name: "docker:dind" }]
},
allow_failure: false,
when: "on_success",
environment: nil,
yaml_variables: []
})
end
it "returns image and service when overridden for job" do
config = YAML.dump({ image: "ruby:2.1",
services: ["mysql"],
before_script: ["pwd"],
rspec: { image: "ruby:2.5", services: ["postgresql", "docker:dind"], script: "rspec" } })
config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref("test", "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"],
image: { name: "ruby:2.5" },
services: [{ name: "postgresql" }, { name: "docker:dind" }]
},
allow_failure: false,
when: "on_success",
environment: nil,
yaml_variables: []
})
end
end
end
describe 'Variables' do
let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config), path) }
subject { config_processor.builds.first[:yaml_variables] }
context 'when global variables are defined' do
let(:variables) do
{ 'VAR1' => 'value1', 'VAR2' => 'value2' }
end
let(:config) do
{
variables: variables,
before_script: ['pwd'],
rspec: { script: 'rspec' }
}
end
it 'returns global variables' do
expect(subject).to contain_exactly(
{ key: 'VAR1', value: 'value1', public: true },
{ key: 'VAR2', value: 'value2', public: true }
)
end
end
context 'when job and global variables are defined' do
let(:global_variables) do
{ 'VAR1' => 'global1', 'VAR3' => 'global3' }
end
let(:job_variables) do
{ 'VAR1' => 'value1', 'VAR2' => 'value2' }
end
let(:config) do
{
before_script: ['pwd'],
variables: global_variables,
rspec: { script: 'rspec', variables: job_variables }
}
end
it 'returns all unique variables' do
expect(subject).to contain_exactly(
{ key: 'VAR3', value: 'global3', public: true },
{ key: 'VAR1', value: 'value1', public: true },
{ key: 'VAR2', value: 'value2', public: true }
)
end
end
context 'when job variables are defined' do
let(:config) do
{
before_script: ['pwd'],
rspec: { script: 'rspec', variables: variables }
}
end
context 'when syntax is correct' do
let(:variables) do
{ 'VAR1' => 'value1', 'VAR2' => 'value2' }
end
it 'returns job variables' do
expect(subject).to contain_exactly(
{ key: 'VAR1', value: 'value1', public: true },
{ key: 'VAR2', value: 'value2', public: true }
)
end
end
context 'when syntax is incorrect' do
context 'when variables defined but invalid' do
let(:variables) do
%w(VAR1 value1 VAR2 value2)
end
it 'raises error' do
expect { subject }
.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError,
/jobs:rspec:variables config should be a hash of key value pairs/)
end
end
context 'when variables key defined but value not specified' do
let(:variables) do
nil
end
it 'returns empty array' do
##
# When variables config is empty, we assume this is a valid
# configuration, see issue #18775
#
expect(subject).to be_an_instance_of(Array)
expect(subject).to be_empty
end
end
end
end
context 'when job variables are not defined' do
let(:config) do
{
before_script: ['pwd'],
rspec: { script: 'rspec' }
}
end
it 'returns empty array' do
expect(subject).to be_an_instance_of(Array)
expect(subject).to be_empty
end
end
end
describe "When" do
%w(on_success on_failure always).each do |when_state|
it "returns #{when_state} when defined" do
config = YAML.dump({
rspec: { script: "rspec", when: when_state }
})
config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
builds = config_processor.builds_for_stage_and_ref("test", "master")
expect(builds.size).to eq(1)
expect(builds.first[:when]).to eq(when_state)
end
end
end
describe 'cache' do
context 'when cache definition has unknown keys' do
it 'raises relevant validation error' do
config = YAML.dump(
{ cache: { untracked: true, invalid: 'key' },
rspec: { script: 'rspec' } })
expect { Gitlab::Ci::YamlProcessor.new(config) }.to raise_error(
Gitlab::Ci::YamlProcessor::ValidationError,
'cache config contains unknown keys: invalid'
)
end
end
it "returns cache when defined globally" do
config = YAML.dump({
cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' },
rspec: {
script: "rspec"
}
})
config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
paths: ["logs/", "binaries/"],
untracked: true,
key: 'key',
policy: 'pull-push'
)
end
it "returns cache when defined in a job" do
config = YAML.dump({
rspec: {
cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'key' },
script: "rspec"
}
})
config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
paths: ["logs/", "binaries/"],
untracked: true,
key: 'key',
policy: 'pull-push'
)
end
it "overwrite cache when defined for a job and globally" do
config = YAML.dump({
cache: { paths: ["logs/", "binaries/"], untracked: true, key: 'global' },
rspec: {
script: "rspec",
cache: { paths: ["test/"], untracked: false, key: 'local' }
}
})
config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref("test", "master").first[:options][:cache]).to eq(
paths: ["test/"],
untracked: false,
key: 'local',
policy: 'pull-push'
)
end
end
describe "Artifacts" do
it "returns artifacts when defined" do
config = YAML.dump({
image: "ruby:2.1",
services: ["mysql"],
before_script: ["pwd"],
rspec: {
artifacts: {
paths: ["logs/", "binaries/"],
untracked: true,
name: "custom_name",
expire_in: "7d"
},
script: "rspec"
}
})
config_processor = Gitlab::Ci::YamlProcessor.new(config)
expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
expect(config_processor.builds_for_stage_and_ref("test", "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"],
image: { name: "ruby:2.1" },
services: [{ name: "mysql" }],
artifacts: {
name: "custom_name",
paths: ["logs/", "binaries/"],
untracked: true,
expire_in: "7d"
}
},
when: "on_success",
allow_failure: false,
environment: nil,
yaml_variables: []
})
end
%w[on_success on_failure always].each do |when_state|
it "returns artifacts for when #{when_state} defined" do
config = YAML.dump({
rspec: {
script: "rspec",
artifacts: { paths: ["logs/", "binaries/"], when: when_state }
}
})
config_processor = Gitlab::Ci::YamlProcessor.new(config, path)
builds = config_processor.builds_for_stage_and_ref("test", "master")
expect(builds.size).to eq(1)
expect(builds.first[:options][:artifacts][:when]).to eq(when_state)
end
end
end
describe '#environment' do
let(:config) do
{
deploy_to_production: { stage: 'deploy', script: 'test', environment: environment }
}
end
let(:processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
let(:builds) { processor.builds_for_stage_and_ref('deploy', 'master') }
context 'when a production environment is specified' do
let(:environment) { 'production' }
it 'does return production' do
expect(builds.size).to eq(1)
expect(builds.first[:environment]).to eq(environment)
expect(builds.first[:options]).to include(environment: { name: environment, action: "start" })
end
end
context 'when hash is specified' do
let(:environment) do
{ name: 'production',
url: 'http://production.gitlab.com' }
end
it 'does return production and URL' do
expect(builds.size).to eq(1)
expect(builds.first[:environment]).to eq(environment[:name])
expect(builds.first[:options]).to include(environment: environment)
end
context 'the url has a port as variable' do
let(:environment) do
{ name: 'production',
url: 'http://production.gitlab.com:$PORT' }
end
it 'allows a variable for the port' do
expect(builds.size).to eq(1)
expect(builds.first[:environment]).to eq(environment[:name])
expect(builds.first[:options]).to include(environment: environment)
end
end
end
context 'when no environment is specified' do
let(:environment) { nil }
it 'does return nil environment' do
expect(builds.size).to eq(1)
expect(builds.first[:environment]).to be_nil
end
end
context 'is not a string' do
let(:environment) { 1 }
it 'raises error' do
expect { builds }.to raise_error(
'jobs:deploy_to_production:environment config should be a hash or a string')
end
end
context 'is not a valid string' do
let(:environment) { 'production:staging' }
it 'raises error' do
expect { builds }.to raise_error("jobs:deploy_to_production:environment name #{Gitlab::Regex.environment_name_regex_message}")
end
end
context 'when on_stop is specified' do
let(:review) { { stage: 'deploy', script: 'test', environment: { name: 'review', on_stop: 'close_review' } } }
let(:config) { { review: review, close_review: close_review }.compact }
context 'with matching job' do
let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review', action: 'stop' } } }
it 'does return a list of builds' do
expect(builds.size).to eq(2)
expect(builds.first[:environment]).to eq('review')
end
end
context 'without matching job' do
let(:close_review) { nil }
it 'raises error' do
expect { builds }.to raise_error('review job: on_stop job close_review is not defined')
end
end
context 'with close job without environment' do
let(:close_review) { { stage: 'deploy', script: 'test' } }
it 'raises error' do
expect { builds }.to raise_error('review job: on_stop job close_review does not have environment defined')
end
end
context 'with close job for different environment' do
let(:close_review) { { stage: 'deploy', script: 'test', environment: 'production' } }
it 'raises error' do
expect { builds }.to raise_error('review job: on_stop job close_review have different environment name')
end
end
context 'with close job without stop action' do
let(:close_review) { { stage: 'deploy', script: 'test', environment: { name: 'review' } } }
it 'raises error' do
expect { builds }.to raise_error('review job: on_stop job close_review needs to have action stop defined')
end
end
end
end
describe "Dependencies" do
let(:config) do
{
build1: { stage: 'build', script: 'test' },
build2: { stage: 'build', script: 'test' },
test1: { stage: 'test', script: 'test', dependencies: dependencies },
test2: { stage: 'test', script: 'test' },
deploy: { stage: 'test', script: 'test' }
}
end
subject { Gitlab::Ci::YamlProcessor.new(YAML.dump(config)) }
context 'no dependencies' do
let(:dependencies) { }
it { expect { subject }.not_to raise_error }
end
context 'dependencies to builds' do
let(:dependencies) { %w(build1 build2) }
it { expect { subject }.not_to raise_error }
end
context 'dependencies to builds defined as symbols' do
let(:dependencies) { [:build1, :build2] }
it { expect { subject }.not_to raise_error }
end
context 'undefined dependency' do
let(:dependencies) { ['undefined'] }
it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'test1 job: undefined dependency: undefined') }
end
context 'dependencies to deploy' do
let(:dependencies) { ['deploy'] }
it { expect { subject }.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, 'test1 job: dependency deploy is not defined in prior stages') }
end
end
describe "Hidden jobs" do
let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config) }
subject { config_processor.builds_for_stage_and_ref("test", "master") }
shared_examples 'hidden_job_handling' do
it "doesn't create jobs that start with dot" do
expect(subject.size).to eq(1)
expect(subject.first).to eq({
stage: "test",
stage_idx: 1,
name: "normal_job",
commands: "test",
coverage_regex: nil,
tag_list: [],
options: {
script: ["test"]
},
when: "on_success",
allow_failure: false,
environment: nil,
yaml_variables: []
})
end
end
context 'when hidden job have a script definition' do
let(:config) do
YAML.dump({
'.hidden_job' => { image: 'ruby:2.1', script: 'test' },
'normal_job' => { script: 'test' }
})
end
it_behaves_like 'hidden_job_handling'
end
context "when hidden job doesn't have a script definition" do
let(:config) do
YAML.dump({
'.hidden_job' => { image: 'ruby:2.1' },
'normal_job' => { script: 'test' }
})
end
it_behaves_like 'hidden_job_handling'
end
end
describe "YAML Alias/Anchor" do
let(:config_processor) { Gitlab::Ci::YamlProcessor.new(config) }
subject { config_processor.builds_for_stage_and_ref("build", "master") }
shared_examples 'job_templates_handling' do
it "is correctly supported for jobs" do
expect(subject.size).to eq(2)
expect(subject.first).to eq({
stage: "build",
stage_idx: 0,
name: "job1",
commands: "execute-script-for-job",
coverage_regex: nil,
tag_list: [],
options: {
script: ["execute-script-for-job"]
},
when: "on_success",
allow_failure: false,
environment: nil,
yaml_variables: []
})
expect(subject.second).to eq({
stage: "build",
stage_idx: 0,
name: "job2",
commands: "execute-script-for-job",
coverage_regex: nil,
tag_list: [],
options: {
script: ["execute-script-for-job"]
},
when: "on_success",
allow_failure: false,
environment: nil,
yaml_variables: []
})
end
end
context 'when template is a job' do
let(:config) do
<<EOT
job1: &JOBTMPL
stage: build
script: execute-script-for-job
job2: *JOBTMPL
EOT
end
it_behaves_like 'job_templates_handling'
end
context 'when template is a hidden job' do
let(:config) do
<<EOT
.template: &JOBTMPL
stage: build
script: execute-script-for-job
job1: *JOBTMPL
job2: *JOBTMPL
EOT
end
it_behaves_like 'job_templates_handling'
end
context 'when job adds its own keys to a template definition' do
let(:config) do
<<EOT
.template: &JOBTMPL
stage: build
job1:
<<: *JOBTMPL
script: execute-script-for-job
job2:
<<: *JOBTMPL
script: execute-script-for-job
EOT
end
it_behaves_like 'job_templates_handling'
end
end
describe "Error handling" do
it "fails to parse YAML" do
expect {Gitlab::Ci::YamlProcessor.new("invalid: yaml: test")}.to raise_error(Psych::SyntaxError)
end
it "indicates that object is invalid" do
expect {Gitlab::Ci::YamlProcessor.new("invalid_yaml")}.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError)
end
it "returns errors if tags parameter is invalid" do
config = YAML.dump({ rspec: { script: "test", tags: "mysql" } })
expect do
Gitlab::Ci::YamlProcessor.new(config, path)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec tags should be an array of strings")
end
it "returns errors if before_script parameter is invalid" do
config = YAML.dump({ before_script: "bundle update", rspec: { script: "test" } })
expect do
Gitlab::Ci::YamlProcessor.new(config, path)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "before_script config should be an array of strings")
end
it "returns errors if job before_script parameter is not an array of strings" do
config = YAML.dump({ rspec: { script: "test", before_script: [10, "test"] } })
expect do
Gitlab::Ci::YamlProcessor.new(config, path)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:before_script config should be an array of strings")
end
it "returns errors if after_script parameter is invalid" do
config = YAML.dump({ after_script: "bundle update", rspec: { script: "test" } })
expect do
Gitlab::Ci::YamlProcessor.new(config, path)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "after_script config should be an array of strings")
end
it "returns errors if job after_script parameter is not an array of strings" do
config = YAML.dump({ rspec: { script: "test", after_script: [10, "test"] } })
expect do
Gitlab::Ci::YamlProcessor.new(config, path)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:after_script config should be an array of strings")
end
it "returns errors if image parameter is invalid" do
config = YAML.dump({ image: ["test"], rspec: { script: "test" } })
expect do
Gitlab::Ci::YamlProcessor.new(config, path)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "image config should be a hash or a string")
end
it "returns errors if job name is blank" do
config = YAML.dump({ '' => { script: "test" } })
expect do
Gitlab::Ci::YamlProcessor.new(config, path)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:job name can't be blank")
end
it "returns errors if job name is non-string" do
config = YAML.dump({ 10 => { script: "test" } })
expect do
Gitlab::Ci::YamlProcessor.new(config, path)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:10 name should be a symbol")
end
it "returns errors if job image parameter is invalid" do
config = YAML.dump({ rspec: { script: "test", image: ["test"] } })
expect do
Gitlab::Ci::YamlProcessor.new(config, path)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:image config should be a hash or a string")
end
it "returns errors if services parameter is not an array" do
config = YAML.dump({ services: "test", rspec: { script: "test" } })
expect do
Gitlab::Ci::YamlProcessor.new(config, path)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "services config should be a array")
end
it "returns errors if services parameter is not an array of strings" do
config = YAML.dump({ services: [10, "test"], rspec: { script: "test" } })
expect do
Gitlab::Ci::YamlProcessor.new(config, path)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "service config should be a hash or a string")
end
it "returns errors if job services parameter is not an array" do
config = YAML.dump({ rspec: { script: "test", services: "test" } })
expect do
Gitlab::Ci::YamlProcessor.new(config, path)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:services config should be a array")
end
it "returns errors if job services parameter is not an array of strings" do
config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } })
expect do
Gitlab::Ci::YamlProcessor.new(config, path)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "service config should be a hash or a string")
end
it "returns error if job configuration is invalid" do
config = YAML.dump({ extra: "bundle update" })
expect do
Gitlab::Ci::YamlProcessor.new(config, path)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:extra config should be a hash")
end
it "returns errors if services configuration is not correct" do
config = YAML.dump({ extra: { script: 'rspec', services: "test" } })
expect do
Gitlab::Ci::YamlProcessor.new(config, path)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:extra:services config should be a array")
end
it "returns errors if there are no jobs defined" do
config = YAML.dump({ before_script: ["bundle update"] })
expect do
Gitlab::Ci::YamlProcessor.new(config, path)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs config should contain at least one visible job")
end
it "returns errors if there are no visible jobs defined" do
config = YAML.dump({ before_script: ["bundle update"], '.hidden'.to_sym => { script: 'ls' } })
expect do
Gitlab::Ci::YamlProcessor.new(config, path)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs config should contain at least one visible job")
end
it "returns errors if job allow_failure parameter is not an boolean" do
config = YAML.dump({ rspec: { script: "test", allow_failure: "string" } })
expect do
Gitlab::Ci::YamlProcessor.new(config, path)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec allow failure should be a boolean value")
end
it "returns errors if job stage is not a string" do
config = YAML.dump({ rspec: { script: "test", type: 1 } })
expect do
Gitlab::Ci::YamlProcessor.new(config, path)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:type config should be a string")
end
it "returns errors if job stage is not a pre-defined stage" do
config = YAML.dump({ rspec: { script: "test", type: "acceptance" } })
expect do
Gitlab::Ci::YamlProcessor.new(config, path)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy")
end
it "returns errors if job stage is not a defined stage" do
config = YAML.dump({ types: %w(build test), rspec: { script: "test", type: "acceptance" } })
expect do
Gitlab::Ci::YamlProcessor.new(config, path)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "rspec job: stage parameter should be build, test")
end
it "returns errors if stages is not an array" do
config = YAML.dump({ stages: "test", rspec: { script: "test" } })
expect do
Gitlab::Ci::YamlProcessor.new(config, path)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "stages config should be an array of strings")
end
it "returns errors if stages is not an array of strings" do
config = YAML.dump({ stages: [true, "test"], rspec: { script: "test" } })
expect do
Gitlab::Ci::YamlProcessor.new(config, path)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "stages config should be an array of strings")
end
it "returns errors if variables is not a map" do
config = YAML.dump({ variables: "test", rspec: { script: "test" } })
expect do
Gitlab::Ci::YamlProcessor.new(config, path)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "variables config should be a hash of key value pairs")
end
it "returns errors if variables is not a map of key-value strings" do
config = YAML.dump({ variables: { test: false }, rspec: { script: "test" } })
expect do
Gitlab::Ci::YamlProcessor.new(config, path)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "variables config should be a hash of key value pairs")
end
it "returns errors if job when is not on_success, on_failure or always" do
config = YAML.dump({ rspec: { script: "test", when: 1 } })
expect do
Gitlab::Ci::YamlProcessor.new(config, path)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec when should be on_success, on_failure, always or manual")
end
it "returns errors if job artifacts:name is not an a string" do
config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { name: 1 } } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts name should be a string")
end
it "returns errors if job artifacts:when is not an a predefined value" do
config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { when: 1 } } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts when should be on_success, on_failure or always")
end
it "returns errors if job artifacts:expire_in is not an a string" do
config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: 1 } } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration")
end
it "returns errors if job artifacts:expire_in is not an a valid duration" do
config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { expire_in: "7 elephants" } } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts expire in should be a duration")
end
it "returns errors if job artifacts:untracked is not an array of strings" do
config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { untracked: "string" } } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts untracked should be a boolean value")
end
it "returns errors if job artifacts:paths is not an array of strings" do
config = YAML.dump({ types: %w(build test), rspec: { script: "test", artifacts: { paths: "string" } } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:artifacts paths should be an array of strings")
end
it "returns errors if cache:untracked is not an array of strings" do
config = YAML.dump({ cache: { untracked: "string" }, rspec: { script: "test" } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "cache:untracked config should be a boolean value")
end
it "returns errors if cache:paths is not an array of strings" do
config = YAML.dump({ cache: { paths: "string" }, rspec: { script: "test" } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "cache:paths config should be an array of strings")
end
it "returns errors if cache:key is not a string" do
config = YAML.dump({ cache: { key: 1 }, rspec: { script: "test" } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "cache:key config should be a string or symbol")
end
it "returns errors if job cache:key is not an a string" do
config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { key: 1 } } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:cache:key config should be a string or symbol")
end
it "returns errors if job cache:untracked is not an array of strings" do
config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { untracked: "string" } } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:cache:untracked config should be a boolean value")
end
it "returns errors if job cache:paths is not an array of strings" do
config = YAML.dump({ types: %w(build test), rspec: { script: "test", cache: { paths: "string" } } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec:cache:paths config should be an array of strings")
end
it "returns errors if job dependencies is not an array of strings" do
config = YAML.dump({ types: %w(build test), rspec: { script: "test", dependencies: "string" } })
expect do
Gitlab::Ci::YamlProcessor.new(config)
end.to raise_error(Gitlab::Ci::YamlProcessor::ValidationError, "jobs:rspec dependencies should be an array of strings")
end
end
describe "Validate configuration templates" do
templates = Dir.glob("#{Rails.root.join('vendor/gitlab-ci-yml')}/**/*.gitlab-ci.yml")
templates.each do |file|
it "does not return errors for #{file}" do
file = File.read(file)
expect { Gitlab::Ci::YamlProcessor.new(file) }.not_to raise_error
end
end
end
describe "#validation_message" do
context "when the YAML could not be parsed" do
it "returns an error about invalid configutaion" do
content = YAML.dump("invalid: yaml: test")
expect(Gitlab::Ci::YamlProcessor.validation_message(content))
.to eq "Invalid configuration format"
end
end
context "when the tags parameter is invalid" do
it "returns an error about invalid tags" do
content = YAML.dump({ rspec: { script: "test", tags: "mysql" } })
expect(Gitlab::Ci::YamlProcessor.validation_message(content))
.to eq "jobs:rspec tags should be an array of strings"
end
end
context "when YAML content is empty" do
it "returns an error about missing content" do
expect(Gitlab::Ci::YamlProcessor.validation_message(''))
.to eq "Please provide content of .gitlab-ci.yml"
end
end
context "when the YAML is valid" do
it "does not return any errors" do
content = File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
expect(Gitlab::Ci::YamlProcessor.validation_message(content)).to be_nil
end
end
end
end
end
end
......@@ -119,7 +119,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
it 'has no when YML attributes but only the DB column' do
allow_any_instance_of(Ci::Pipeline).to receive(:ci_yaml_file).and_return(File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')))
expect_any_instance_of(Ci::GitlabCiYamlProcessor).not_to receive(:build_attributes)
expect_any_instance_of(Gitlab::Ci::YamlProcessor).not_to receive(:build_attributes)
saved_project_json
end
......
......@@ -4,7 +4,7 @@ describe 'ci/lints/show' do
include Devise::Test::ControllerHelpers
describe 'XSS protection' do
let(:config_processor) { Ci::GitlabCiYamlProcessor.new(YAML.dump(content)) }
let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(content)) }
before do
assign(:status, true)
assign(:builds, config_processor.builds)
......@@ -59,7 +59,7 @@ describe 'ci/lints/show' do
}
end
let(:config_processor) { Ci::GitlabCiYamlProcessor.new(YAML.dump(content)) }
let(:config_processor) { Gitlab::Ci::YamlProcessor.new(YAML.dump(content)) }
context 'when the content is valid' do
before do
......
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