summaryrefslogtreecommitdiff
path: root/lib/gitlab/ci
diff options
context:
space:
mode:
Diffstat (limited to 'lib/gitlab/ci')
-rw-r--r--lib/gitlab/ci/ansi2html.rb29
-rw-r--r--lib/gitlab/ci/build/policy/variables.rb2
-rw-r--r--lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb30
-rw-r--r--lib/gitlab/ci/build/rules.rb37
-rw-r--r--lib/gitlab/ci/build/rules/rule.rb32
-rw-r--r--lib/gitlab/ci/build/rules/rule/clause.rb31
-rw-r--r--lib/gitlab/ci/build/rules/rule/clause/changes.rb23
-rw-r--r--lib/gitlab/ci/build/rules/rule/clause/if.rb19
-rw-r--r--lib/gitlab/ci/charts.rb14
-rw-r--r--lib/gitlab/ci/config/entry/job.rb52
-rw-r--r--lib/gitlab/ci/config/entry/rules.rb33
-rw-r--r--lib/gitlab/ci/config/entry/rules/rule.rb42
-rw-r--r--lib/gitlab/ci/config/normalizer.rb64
-rw-r--r--lib/gitlab/ci/pipeline/chain/command.rb6
-rw-r--r--lib/gitlab/ci/pipeline/chain/populate.rb11
-rw-r--r--lib/gitlab/ci/pipeline/chain/validate/abilities.rb4
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/matches.rb3
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb3
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb11
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexer.rb17
-rw-r--r--lib/gitlab/ci/pipeline/expression/parser.rb39
-rw-r--r--lib/gitlab/ci/pipeline/seed/base.rb4
-rw-r--r--lib/gitlab/ci/pipeline/seed/build.rb127
-rw-r--r--lib/gitlab/ci/pipeline/seed/stage.rb25
-rw-r--r--lib/gitlab/ci/status/build/manual.rb2
-rw-r--r--lib/gitlab/ci/status/factory.rb6
-rw-r--r--lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml5
-rw-r--r--lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml420
-rw-r--r--lib/gitlab/ci/templates/Maven.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Packer.gitlab-ci.yml2
-rw-r--r--lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml7
-rw-r--r--lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml4
-rw-r--r--lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml4
-rw-r--r--lib/gitlab/ci/templates/Serverless.gitlab-ci.yml13
-rw-r--r--lib/gitlab/ci/trace.rb14
-rw-r--r--lib/gitlab/ci/trace/chunked_io.rb7
-rw-r--r--lib/gitlab/ci/yaml_processor.rb25
40 files changed, 623 insertions, 552 deletions
diff --git a/lib/gitlab/ci/ansi2html.rb b/lib/gitlab/ci/ansi2html.rb
index fc3223e7442..b7886114e9c 100644
--- a/lib/gitlab/ci/ansi2html.rb
+++ b/lib/gitlab/ci/ansi2html.rb
@@ -194,16 +194,10 @@ module Gitlab
end
def handle_new_line
- css_classes = []
-
- if @sections.any?
- css_classes = %w[section line] + sections.map { |section| "s_#{section}" }
- end
-
write_in_tag %{<br/>}
- write_raw %{<span class="#{css_classes.join(' ')}"></span>} if css_classes.any?
+
+ close_open_tags if @sections.any? && @lineno_in_section == 0
@lineno_in_section += 1
- open_new_tag
end
def handle_section(scanner)
@@ -224,7 +218,7 @@ module Gitlab
return if @sections.include?(section)
@sections << section
- write_raw %{<div class="js-section-start fa fa-caret-down append-right-8 cursor-pointer" data-timestamp="#{timestamp}" data-section="#{data_section_names}" role="button"></div>}
+ write_raw %{<div class="js-section-start section-start fa fa-caret-down pr-2 cursor-pointer" data-timestamp="#{timestamp}" data-section="#{data_section_names}" role="button"></div>}
@lineno_in_section = 0
end
@@ -310,11 +304,24 @@ module Gitlab
if @sections.any?
css_classes << "section"
- css_classes << "js-section-header section-header" if @lineno_in_section == 0
+
+ css_classes << if @lineno_in_section == 0
+ "js-section-header section-header cursor-pointer"
+ else
+ "line"
+ end
+
css_classes += sections.map { |section| "js-s-#{section}" }
end
- @out << %{<span class="#{css_classes.join(' ')}">}
+ close_open_tags
+
+ @out << if css_classes.any?
+ %{<span class="#{css_classes.join(' ')}">}
+ else
+ %{<span>}
+ end
+
@n_open_tags += 1
end
diff --git a/lib/gitlab/ci/build/policy/variables.rb b/lib/gitlab/ci/build/policy/variables.rb
index 0698136166a..e9c8864123f 100644
--- a/lib/gitlab/ci/build/policy/variables.rb
+++ b/lib/gitlab/ci/build/policy/variables.rb
@@ -10,7 +10,7 @@ module Gitlab
end
def satisfied_by?(pipeline, seed)
- variables = seed.to_resource.scoped_variables_hash
+ variables = seed.scoped_variables_hash
statements = @expressions.map do |statement|
::Gitlab::Ci::Pipeline::Expression::Statement
diff --git a/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb b/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb
index e6e0aaab60b..6ab4fca3854 100644
--- a/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb
+++ b/lib/gitlab/ci/build/prerequisite/kubernetes_namespace.rb
@@ -8,31 +8,51 @@ module Gitlab
def unmet?
deployment_cluster.present? &&
deployment_cluster.managed? &&
- (kubernetes_namespace.new_record? || kubernetes_namespace.service_account_token.blank?)
+ missing_namespace?
end
def complete!
return unless unmet?
- create_or_update_namespace
+ create_namespace
end
private
+ def missing_namespace?
+ kubernetes_namespace.nil? || kubernetes_namespace.service_account_token.blank?
+ end
+
def deployment_cluster
build.deployment&.cluster
end
+ def environment
+ build.deployment.environment
+ end
+
def kubernetes_namespace
strong_memoize(:kubernetes_namespace) do
- deployment_cluster.find_or_initialize_kubernetes_namespace_for_project(build.project)
+ Clusters::KubernetesNamespaceFinder.new(
+ deployment_cluster,
+ project: environment.project,
+ environment_slug: environment.slug,
+ allow_blank_token: true
+ ).execute
end
end
- def create_or_update_namespace
+ def create_namespace
Clusters::Gcp::Kubernetes::CreateOrUpdateNamespaceService.new(
cluster: deployment_cluster,
- kubernetes_namespace: kubernetes_namespace
+ kubernetes_namespace: kubernetes_namespace || build_namespace_record
+ ).execute
+ end
+
+ def build_namespace_record
+ Clusters::BuildKubernetesNamespaceService.new(
+ deployment_cluster,
+ environment: environment
).execute
end
end
diff --git a/lib/gitlab/ci/build/rules.rb b/lib/gitlab/ci/build/rules.rb
new file mode 100644
index 00000000000..89623a809c9
--- /dev/null
+++ b/lib/gitlab/ci/build/rules.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Build
+ class Rules
+ include ::Gitlab::Utils::StrongMemoize
+
+ Result = Struct.new(:when, :start_in)
+
+ def initialize(rule_hashes, default_when = 'on_success')
+ @rule_list = Rule.fabricate_list(rule_hashes)
+ @default_when = default_when
+ end
+
+ def evaluate(pipeline, build)
+ if @rule_list.nil?
+ Result.new(@default_when)
+ elsif matched_rule = match_rule(pipeline, build)
+ Result.new(
+ matched_rule.attributes[:when] || @default_when,
+ matched_rule.attributes[:start_in]
+ )
+ else
+ Result.new('never')
+ end
+ end
+
+ private
+
+ def match_rule(pipeline, build)
+ @rule_list.find { |rule| rule.matches?(pipeline, build) }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/rules/rule.rb b/lib/gitlab/ci/build/rules/rule.rb
new file mode 100644
index 00000000000..8d52158c8d2
--- /dev/null
+++ b/lib/gitlab/ci/build/rules/rule.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Build
+ class Rules::Rule
+ attr_accessor :attributes
+
+ def self.fabricate_list(list)
+ list.map(&method(:new)) if list
+ end
+
+ def initialize(spec)
+ @clauses = []
+ @attributes = {}
+
+ spec.each do |type, value|
+ if clause = Clause.fabricate(type, value)
+ @clauses << clause
+ else
+ @attributes.merge!(type => value)
+ end
+ end
+ end
+
+ def matches?(pipeline, build)
+ @clauses.all? { |clause| clause.satisfied_by?(pipeline, build) }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/rules/rule/clause.rb b/lib/gitlab/ci/build/rules/rule/clause.rb
new file mode 100644
index 00000000000..ff0baf3348c
--- /dev/null
+++ b/lib/gitlab/ci/build/rules/rule/clause.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Build
+ class Rules::Rule::Clause
+ ##
+ # Abstract class that defines an interface of a single
+ # job rule specification.
+ #
+ # Used for job's inclusion rules configuration.
+ #
+ UnknownClauseError = Class.new(StandardError)
+
+ def self.fabricate(type, value)
+ type = type.to_s.camelize
+
+ self.const_get(type).new(value) if self.const_defined?(type)
+ end
+
+ def initialize(spec)
+ @spec = spec
+ end
+
+ def satisfied_by?(pipeline, seed = nil)
+ raise NotImplementedError
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/rules/rule/clause/changes.rb b/lib/gitlab/ci/build/rules/rule/clause/changes.rb
new file mode 100644
index 00000000000..81d2ee6c24c
--- /dev/null
+++ b/lib/gitlab/ci/build/rules/rule/clause/changes.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Build
+ class Rules::Rule::Clause::Changes < Rules::Rule::Clause
+ def initialize(globs)
+ @globs = Array(globs)
+ end
+
+ def satisfied_by?(pipeline, seed)
+ return true if pipeline.modified_paths.nil?
+
+ pipeline.modified_paths.any? do |path|
+ @globs.any? do |glob|
+ File.fnmatch?(glob, path, File::FNM_PATHNAME | File::FNM_DOTMATCH | File::FNM_EXTGLOB)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/build/rules/rule/clause/if.rb b/lib/gitlab/ci/build/rules/rule/clause/if.rb
new file mode 100644
index 00000000000..18c3b450f95
--- /dev/null
+++ b/lib/gitlab/ci/build/rules/rule/clause/if.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ module Build
+ class Rules::Rule::Clause::If < Rules::Rule::Clause
+ def initialize(expression)
+ @expression = expression
+ end
+
+ def satisfied_by?(pipeline, seed)
+ variables = seed.scoped_variables_hash
+
+ ::Gitlab::Ci::Pipeline::Expression::Statement.new(@expression, variables).truthful?
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/charts.rb b/lib/gitlab/ci/charts.rb
index 7cabaadb122..3fbfdffe277 100644
--- a/lib/gitlab/ci/charts.rb
+++ b/lib/gitlab/ci/charts.rb
@@ -21,16 +21,10 @@ module Gitlab
module MonthlyInterval
# rubocop: disable CodeReuse/ActiveRecord
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
+ query
+ .group("to_char(#{::Ci::Pipeline.table_name}.created_at, '01 Month YYYY')")
+ .count(:created_at)
+ .transform_keys(&:squish)
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb
index 5ab795359b8..6e11c582750 100644
--- a/lib/gitlab/ci/config/entry/job.rb
+++ b/lib/gitlab/ci/config/entry/job.rb
@@ -11,17 +11,28 @@ module Gitlab
include ::Gitlab::Config::Entry::Configurable
include ::Gitlab::Config::Entry::Attributable
- ALLOWED_KEYS = %i[tags script only except type image services
+ ALLOWED_WHEN = %w[on_success on_failure always manual delayed].freeze
+ ALLOWED_KEYS = %i[tags script only except rules type image services
allow_failure type stage when start_in artifacts cache
- dependencies before_script after_script variables
+ dependencies needs before_script after_script variables
environment coverage retry parallel extends].freeze
+ REQUIRED_BY_NEEDS = %i[stage].freeze
+
validations do
+ validates :config, type: Hash
validates :config, allowed_keys: ALLOWED_KEYS
+ validates :config, required_keys: REQUIRED_BY_NEEDS, if: :has_needs?
validates :config, presence: true
validates :script, presence: true
validates :name, presence: true
validates :name, type: Symbol
+ validates :config,
+ disallowed_keys: {
+ in: %i[only except when start_in],
+ message: 'key may not be used with `rules`'
+ },
+ if: :has_rules?
with_options allow_nil: true do
validates :tags, array_of_strings: true
@@ -29,16 +40,29 @@ module Gitlab
validates :parallel, numericality: { only_integer: true,
greater_than_or_equal_to: 2,
less_than_or_equal_to: 50 }
- validates :when,
- inclusion: { in: %w[on_success on_failure always manual delayed],
- message: 'should be on_success, on_failure, ' \
- 'always, manual or delayed' }
+ validates :when, inclusion: {
+ in: ALLOWED_WHEN,
+ message: "should be one of: #{ALLOWED_WHEN.join(', ')}"
+ }
+
validates :dependencies, array_of_strings: true
+ validates :needs, array_of_strings: true
validates :extends, array_of_strings_or_string: true
+ validates :rules, array_of_hashes: true
end
validates :start_in, duration: { limit: '1 day' }, if: :delayed?
- validates :start_in, absence: true, unless: :delayed?
+ validates :start_in, absence: true, if: -> { has_rules? || !delayed? }
+
+ validate do
+ next unless dependencies.present?
+ next unless needs.present?
+
+ missing_needs = dependencies - needs
+ if missing_needs.any?
+ errors.add(:dependencies, "the #{missing_needs.join(", ")} should be part of needs")
+ end
+ end
end
entry :before_script, Entry::Script,
@@ -77,6 +101,9 @@ module Gitlab
entry :except, Entry::Policy,
description: 'Refs policy this job will be executed for.'
+ entry :rules, Entry::Rules,
+ description: 'List of evaluable Rules to determine job inclusion.'
+
entry :variables, Entry::Variables,
description: 'Environment variables available for this job.'
@@ -95,10 +122,10 @@ module Gitlab
helpers :before_script, :script, :stage, :type, :after_script,
:cache, :image, :services, :only, :except, :variables,
:artifacts, :environment, :coverage, :retry,
- :parallel
+ :parallel, :needs
attributes :script, :tags, :allow_failure, :when, :dependencies,
- :retry, :parallel, :extends, :start_in
+ :needs, :retry, :parallel, :extends, :start_in, :rules
def self.matching?(name, config)
!name.to_s.start_with?('.') &&
@@ -137,6 +164,10 @@ module Gitlab
self.when == 'delayed'
end
+ def has_rules?
+ @config.try(:key?, :rules)
+ end
+
def ignored?
allow_failure.nil? ? manual_action? : allow_failure
end
@@ -178,7 +209,8 @@ module Gitlab
parallel: parallel_defined? ? parallel_value.to_i : nil,
artifacts: artifacts_value,
after_script: after_script_value,
- ignore: ignored? }
+ ignore: ignored?,
+ needs: needs_defined? ? needs_value : nil }
end
end
end
diff --git a/lib/gitlab/ci/config/entry/rules.rb b/lib/gitlab/ci/config/entry/rules.rb
new file mode 100644
index 00000000000..65cad0880f5
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/rules.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ class Rules < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+
+ validations do
+ validates :config, presence: true
+ validates :config, type: Array
+ end
+
+ def compose!(deps = nil)
+ super(deps) do
+ @config.each_with_index do |rule, index|
+ @entries[index] = ::Gitlab::Config::Entry::Factory.new(Entry::Rules::Rule)
+ .value(rule)
+ .with(key: "rule", parent: self, description: "rule definition.") # rubocop:disable CodeReuse/ActiveRecord
+ .create!
+ end
+
+ @entries.each_value do |entry|
+ entry.compose!(deps)
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/entry/rules/rule.rb b/lib/gitlab/ci/config/entry/rules/rule.rb
new file mode 100644
index 00000000000..1f2a34ec90e
--- /dev/null
+++ b/lib/gitlab/ci/config/entry/rules/rule.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Ci
+ class Config
+ module Entry
+ class Rules::Rule < ::Gitlab::Config::Entry::Node
+ include ::Gitlab::Config::Entry::Validatable
+ include ::Gitlab::Config::Entry::Attributable
+
+ CLAUSES = %i[if changes].freeze
+ ALLOWED_KEYS = %i[if changes when start_in].freeze
+ ALLOWED_WHEN = %w[on_success on_failure always never manual delayed].freeze
+
+ attributes :if, :changes, :when, :start_in
+
+ validations do
+ validates :config, presence: true
+ validates :config, type: { with: Hash }
+ validates :config, allowed_keys: ALLOWED_KEYS
+ validates :config, disallowed_keys: %i[start_in], unless: :specifies_delay?
+ validates :start_in, presence: true, if: :specifies_delay?
+ validates :start_in, duration: { limit: '1 day' }, if: :specifies_delay?
+
+ with_options allow_nil: true do
+ validates :if, expression: true
+ validates :changes, array_of_strings: true
+ validates :when, allowed_values: { in: ALLOWED_WHEN }
+ end
+ end
+
+ def specifies_delay?
+ self.when == 'delayed'
+ end
+
+ def default
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/config/normalizer.rb b/lib/gitlab/ci/config/normalizer.rb
index 191f5d09645..09f9bf5f69f 100644
--- a/lib/gitlab/ci/config/normalizer.rb
+++ b/lib/gitlab/ci/config/normalizer.rb
@@ -4,61 +4,63 @@ module Gitlab
module Ci
class Config
class Normalizer
+ include Gitlab::Utils::StrongMemoize
+
def initialize(jobs_config)
@jobs_config = jobs_config
end
def normalize_jobs
- extract_parallelized_jobs!
- return @jobs_config if @parallelized_jobs.empty?
+ return @jobs_config if parallelized_jobs.empty?
+
+ expand_parallelize_jobs do |job_name, config|
+ if config[:dependencies]
+ config[:dependencies] = expand_names(config[:dependencies])
+ end
- parallelized_config = parallelize_jobs
- parallelize_dependencies(parallelized_config)
+ if config[:needs]
+ config[:needs] = expand_names(config[:needs])
+ end
+
+ config
+ end
end
private
- def extract_parallelized_jobs!
- @parallelized_jobs = {}
+ def expand_names(job_names)
+ return unless job_names
- @jobs_config.each do |job_name, config|
- if config[:parallel]
- @parallelized_jobs[job_name] = self.class.parallelize_job_names(job_name, config[:parallel])
- end
+ job_names.flat_map do |job_name|
+ parallelized_jobs[job_name.to_sym] || job_name
end
-
- @parallelized_jobs
end
- def parallelize_jobs
- @jobs_config.each_with_object({}) do |(job_name, config), hash|
- if @parallelized_jobs.key?(job_name)
- @parallelized_jobs[job_name].each { |name, index| hash[name.to_sym] = config.merge(name: name, instance: index) }
- else
- hash[job_name] = config
- end
+ def parallelized_jobs
+ strong_memoize(:parallelized_jobs) do
+ @jobs_config.each_with_object({}) do |(job_name, config), hash|
+ next unless config[:parallel]
- hash
+ hash[job_name] = self.class.parallelize_job_names(job_name, config[:parallel])
+ end
end
end
- def parallelize_dependencies(parallelized_config)
- parallelized_job_names = @parallelized_jobs.keys.map(&:to_s)
- parallelized_config.each_with_object({}) do |(job_name, config), hash|
- if config[:dependencies] && (intersection = config[:dependencies] & parallelized_job_names).any?
- parallelized_deps = intersection.map { |dep| @parallelized_jobs[dep.to_sym].map(&:first) }.flatten
- deps = config[:dependencies] - intersection + parallelized_deps
- hash[job_name] = config.merge(dependencies: deps)
+ def expand_parallelize_jobs
+ @jobs_config.each_with_object({}) do |(job_name, config), hash|
+ if parallelized_jobs.key?(job_name)
+ parallelized_jobs[job_name].each_with_index do |name, index|
+ hash[name.to_sym] =
+ yield(name, config.merge(name: name, instance: index + 1))
+ end
else
- hash[job_name] = config
+ hash[job_name] = yield(job_name, config)
end
-
- hash
end
end
def self.parallelize_job_names(name, total)
- Array.new(total) { |index| ["#{name} #{index + 1}/#{total}", index + 1] }
+ Array.new(total) { |index| "#{name} #{index + 1}/#{total}" }
end
end
end
diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb
index c911bfa7ff6..afad391e8e0 100644
--- a/lib/gitlab/ci/pipeline/chain/command.rb
+++ b/lib/gitlab/ci/pipeline/chain/command.rb
@@ -20,6 +20,12 @@ module Gitlab
end
end
+ def uses_unsupported_legacy_trigger?
+ trigger_request.present? &&
+ trigger_request.trigger.legacy? &&
+ !trigger_request.trigger.supports_legacy_tokens?
+ end
+
def branch_exists?
strong_memoize(:is_branch) do
project.repository.branch_exists?(ref)
diff --git a/lib/gitlab/ci/pipeline/chain/populate.rb b/lib/gitlab/ci/pipeline/chain/populate.rb
index 0405292a25b..65029f5ce7f 100644
--- a/lib/gitlab/ci/pipeline/chain/populate.rb
+++ b/lib/gitlab/ci/pipeline/chain/populate.rb
@@ -23,12 +23,17 @@ module Gitlab
@command.seeds_block&.call(pipeline)
##
- # Populate pipeline with all stages, and stages with builds.
+ # Gather all runtime build/stage errors
#
- pipeline.stage_seeds.each do |stage|
- pipeline.stages << stage.to_resource
+ if seeds_errors = pipeline.stage_seeds.flat_map(&:errors).compact.presence
+ return error(seeds_errors.join("\n"))
end
+ ##
+ # Populate pipeline with all stages, and stages with builds.
+ #
+ pipeline.stages = pipeline.stage_seeds.map(&:to_resource)
+
if pipeline.stages.none?
return error('No stages / jobs for this pipeline.')
end
diff --git a/lib/gitlab/ci/pipeline/chain/validate/abilities.rb b/lib/gitlab/ci/pipeline/chain/validate/abilities.rb
index aaa3daddcc5..357a1d55b3b 100644
--- a/lib/gitlab/ci/pipeline/chain/validate/abilities.rb
+++ b/lib/gitlab/ci/pipeline/chain/validate/abilities.rb
@@ -14,6 +14,10 @@ module Gitlab
return error('Pipelines are disabled!')
end
+ if @command.uses_unsupported_legacy_trigger?
+ return error('Trigger token is invalid because is not owned by any user')
+ end
+
unless allowed_to_trigger_pipeline?
if can?(current_user, :create_pipeline, project)
return error("Insufficient permissions for protected ref '#{command.ref}'")
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb b/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb
index 942e4e55323..f7b0720d4a9 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/matches.rb
@@ -11,8 +11,9 @@ module Gitlab
def evaluate(variables = {})
text = @left.evaluate(variables)
regexp = @right.evaluate(variables)
+ return false unless regexp
- regexp.scan(text.to_s).any?
+ regexp.scan(text.to_s).present?
end
def self.build(_value, behind, ahead)
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb b/lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb
index 831c27fa0ea..02479ed28a4 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb
@@ -11,8 +11,9 @@ module Gitlab
def evaluate(variables = {})
text = @left.evaluate(variables)
regexp = @right.evaluate(variables)
+ return true unless regexp
- regexp.scan(text.to_s).none?
+ regexp.scan(text.to_s).empty?
end
def self.build(_value, behind, ahead)
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb b/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb
index e4cf360a1c1..0212fa9d661 100644
--- a/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb
@@ -8,11 +8,10 @@ module Gitlab
require_dependency 're2'
class Pattern < Lexeme::Value
- PATTERN = %r{^/.+/[ismU]*$}.freeze
- NEW_PATTERN = %r{^\/([^\/]|\\/)+[^\\]\/[ismU]*}.freeze
+ PATTERN = %r{^\/([^\/]|\\/)+[^\\]\/[ismU]*}.freeze
def initialize(regexp)
- @value = self.class.eager_matching_with_escape_characters? ? regexp.gsub(/\\\//, '/') : regexp
+ @value = regexp.gsub(/\\\//, '/')
unless Gitlab::UntrustedRegexp::RubySyntax.valid?(@value)
raise Lexer::SyntaxError, 'Invalid regular expression!'
@@ -26,16 +25,12 @@ module Gitlab
end
def self.pattern
- eager_matching_with_escape_characters? ? NEW_PATTERN : PATTERN
+ PATTERN
end
def self.build(string)
new(string)
end
-
- def self.eager_matching_with_escape_characters?
- Feature.enabled?(:ci_variables_complex_expressions, default_enabled: true)
- end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/expression/lexer.rb b/lib/gitlab/ci/pipeline/expression/lexer.rb
index 22c210ae26b..7d7582612f9 100644
--- a/lib/gitlab/ci/pipeline/expression/lexer.rb
+++ b/lib/gitlab/ci/pipeline/expression/lexer.rb
@@ -17,17 +17,6 @@ module Gitlab
Expression::Lexeme::Equals,
Expression::Lexeme::Matches,
Expression::Lexeme::NotEquals,
- Expression::Lexeme::NotMatches
- ].freeze
-
- NEW_LEXEMES = [
- Expression::Lexeme::Variable,
- Expression::Lexeme::String,
- Expression::Lexeme::Pattern,
- Expression::Lexeme::Null,
- Expression::Lexeme::Equals,
- Expression::Lexeme::Matches,
- Expression::Lexeme::NotEquals,
Expression::Lexeme::NotMatches,
Expression::Lexeme::And,
Expression::Lexeme::Or
@@ -58,7 +47,7 @@ module Gitlab
return tokens if @scanner.eos?
- lexeme = available_lexemes.find do |type|
+ lexeme = LEXEMES.find do |type|
type.scan(@scanner).tap do |token|
tokens.push(token) if token.present?
end
@@ -71,10 +60,6 @@ module Gitlab
raise Lexer::SyntaxError, 'Too many tokens!'
end
-
- def available_lexemes
- Feature.enabled?(:ci_variables_complex_expressions, default_enabled: true) ? NEW_LEXEMES : LEXEMES
- end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/expression/parser.rb b/lib/gitlab/ci/pipeline/expression/parser.rb
index 589bf32a4d7..edb55edf356 100644
--- a/lib/gitlab/ci/pipeline/expression/parser.rb
+++ b/lib/gitlab/ci/pipeline/expression/parser.rb
@@ -13,39 +13,6 @@ module Gitlab
end
def tree
- if Feature.enabled?(:ci_variables_complex_expressions, default_enabled: true)
- rpn_parse_tree
- else
- reverse_descent_parse_tree
- end
- end
-
- def self.seed(statement)
- new(Expression::Lexer.new(statement).tokens)
- end
-
- private
-
- # This produces a reverse descent parse tree.
- # It does not support precedence of operators.
- def reverse_descent_parse_tree
- while token = @tokens.next
- case token.type
- when :operator
- token.build(@nodes.pop, tree).tap do |node|
- @nodes.push(node)
- end
- when :value
- token.build.tap do |leaf|
- @nodes.push(leaf)
- end
- end
- end
- rescue StopIteration
- @nodes.last || Lexeme::Null.new
- end
-
- def rpn_parse_tree
results = []
tokens_rpn.each do |token|
@@ -70,6 +37,12 @@ module Gitlab
results.pop
end
+ def self.seed(statement)
+ new(Expression::Lexer.new(statement).tokens)
+ end
+
+ private
+
# Parse the expression into Reverse Polish Notation
# (See: Shunting-yard algorithm)
def tokens_rpn
diff --git a/lib/gitlab/ci/pipeline/seed/base.rb b/lib/gitlab/ci/pipeline/seed/base.rb
index 1fd3a61017f..e9e22569ae0 100644
--- a/lib/gitlab/ci/pipeline/seed/base.rb
+++ b/lib/gitlab/ci/pipeline/seed/base.rb
@@ -13,6 +13,10 @@ module Gitlab
raise NotImplementedError
end
+ def errors
+ raise NotImplementedError
+ end
+
def to_resource
raise NotImplementedError
end
diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb
index d8296940a04..1066331062b 100644
--- a/lib/gitlab/ci/pipeline/seed/build.rb
+++ b/lib/gitlab/ci/pipeline/seed/build.rb
@@ -7,39 +7,65 @@ module Gitlab
class Build < Seed::Base
include Gitlab::Utils::StrongMemoize
- delegate :dig, to: :@attributes
+ delegate :dig, to: :@seed_attributes
- def initialize(pipeline, attributes)
+ # When the `ci_dag_limit_needs` is enabled it uses the lower limit
+ LOW_NEEDS_LIMIT = 5
+ HARD_NEEDS_LIMIT = 50
+
+ def initialize(pipeline, attributes, previous_stages)
@pipeline = pipeline
- @attributes = attributes
+ @seed_attributes = attributes
+ @previous_stages = previous_stages
+ @needs_attributes = dig(:needs_attributes)
+
+ @using_rules = attributes.key?(:rules)
+ @using_only = attributes.key?(:only)
+ @using_except = attributes.key?(:except)
@only = Gitlab::Ci::Build::Policy
.fabricate(attributes.delete(:only))
@except = Gitlab::Ci::Build::Policy
.fabricate(attributes.delete(:except))
+ @rules = Gitlab::Ci::Build::Rules
+ .new(attributes.delete(:rules))
+ end
+
+ def name
+ dig(:name)
end
def included?
strong_memoize(:inclusion) do
- @only.all? { |spec| spec.satisfied_by?(@pipeline, self) } &&
- @except.none? { |spec| spec.satisfied_by?(@pipeline, self) }
+ if @using_rules
+ included_by_rules?
+ elsif @using_only || @using_except
+ all_of_only? && none_of_except?
+ else
+ true
+ end
+ end
+ end
+
+ def errors
+ return unless included?
+
+ strong_memoize(:errors) do
+ needs_errors
end
end
def attributes
- @attributes.merge(
- pipeline: @pipeline,
- project: @pipeline.project,
- user: @pipeline.user,
- ref: @pipeline.ref,
- tag: @pipeline.tag,
- trigger_request: @pipeline.legacy_trigger,
- protected: @pipeline.protected_ref?
- )
+ @seed_attributes
+ .deep_merge(pipeline_attributes)
+ .deep_merge(rules_attributes)
end
def bridge?
- @attributes.to_h.dig(:options, :trigger).present?
+ attributes_hash = @seed_attributes.to_h
+ attributes_hash.dig(:options, :trigger).present? ||
+ (attributes_hash.dig(:options, :bridge_needs).instance_of?(Hash) &&
+ attributes_hash.dig(:options, :bridge_needs, :pipeline).present?)
end
def to_resource
@@ -51,6 +77,77 @@ module Gitlab
end
end
end
+
+ def scoped_variables_hash
+ strong_memoize(:scoped_variables_hash) do
+ # This is a temporary piece of technical debt to allow us access
+ # to the CI variables to evaluate rules before we persist a Build
+ # with the result. We should refactor away the extra Build.new,
+ # but be able to get CI Variables directly from the Seed::Build.
+ ::Ci::Build.new(
+ @seed_attributes.merge(pipeline_attributes)
+ ).scoped_variables_hash
+ end
+ end
+
+ private
+
+ def all_of_only?
+ @only.all? { |spec| spec.satisfied_by?(@pipeline, self) }
+ end
+
+ def none_of_except?
+ @except.none? { |spec| spec.satisfied_by?(@pipeline, self) }
+ end
+
+ def needs_errors
+ return if @needs_attributes.nil?
+
+ if @needs_attributes.size > max_needs_allowed
+ return [
+ "#{name}: one job can only need #{max_needs_allowed} others, but you have listed #{@needs_attributes.size}. " \
+ "See needs keyword documentation for more details"
+ ]
+ end
+
+ @needs_attributes.flat_map do |need|
+ result = @previous_stages.any? do |stage|
+ stage.seeds_names.include?(need[:name])
+ end
+
+ "#{name}: needs '#{need[:name]}'" unless result
+ end.compact
+ end
+
+ def max_needs_allowed
+ if Feature.enabled?(:ci_dag_limit_needs, @project, default_enabled: true)
+ LOW_NEEDS_LIMIT
+ else
+ HARD_NEEDS_LIMIT
+ end
+ end
+
+ def pipeline_attributes
+ {
+ pipeline: @pipeline,
+ project: @pipeline.project,
+ user: @pipeline.user,
+ ref: @pipeline.ref,
+ tag: @pipeline.tag,
+ trigger_request: @pipeline.legacy_trigger,
+ protected: @pipeline.protected_ref?
+ }
+ end
+
+ def included_by_rules?
+ rules_attributes[:when] != 'never'
+ end
+
+ def rules_attributes
+ strong_memoize(:rules_attributes) do
+ @using_rules ? @rules.evaluate(@pipeline, self).to_h.compact : {}
+ end
+ end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/seed/stage.rb b/lib/gitlab/ci/pipeline/seed/stage.rb
index 9c15064756a..b600df2f656 100644
--- a/lib/gitlab/ci/pipeline/seed/stage.rb
+++ b/lib/gitlab/ci/pipeline/seed/stage.rb
@@ -10,12 +10,13 @@ module Gitlab
delegate :size, to: :seeds
delegate :dig, to: :seeds
- def initialize(pipeline, attributes)
+ def initialize(pipeline, attributes, previous_stages)
@pipeline = pipeline
@attributes = attributes
+ @previous_stages = previous_stages
@builds = attributes.fetch(:builds).map do |attributes|
- Seed::Build.new(@pipeline, attributes)
+ Seed::Build.new(@pipeline, attributes, previous_stages)
end
end
@@ -32,6 +33,18 @@ module Gitlab
end
end
+ def errors
+ strong_memoize(:errors) do
+ seeds.flat_map(&:errors).compact
+ end
+ end
+
+ def seeds_names
+ strong_memoize(:seeds_names) do
+ seeds.map(&:name).to_set
+ end
+ end
+
def included?
seeds.any?
end
@@ -39,13 +52,7 @@ module Gitlab
def to_resource
strong_memoize(:stage) do
::Ci::Stage.new(attributes).tap do |stage|
- seeds.each do |seed|
- if seed.bridge?
- stage.bridges << seed.to_resource
- else
- stage.builds << seed.to_resource
- end
- end
+ stage.statuses = seeds.map(&:to_resource)
end
end
end
diff --git a/lib/gitlab/ci/status/build/manual.rb b/lib/gitlab/ci/status/build/manual.rb
index d01b09f1398..df572188194 100644
--- a/lib/gitlab/ci/status/build/manual.rb
+++ b/lib/gitlab/ci/status/build/manual.rb
@@ -10,7 +10,7 @@ module Gitlab
image: 'illustrations/manual_action.svg',
size: 'svg-394',
title: _('This job requires a manual action'),
- content: _('This job depends on a user to trigger its process. Often they are used to deploy code to production environments')
+ content: _('This job requires manual intervention to start. Before starting this job, you can add variables below for last-minute configuration changes.')
}
end
diff --git a/lib/gitlab/ci/status/factory.rb b/lib/gitlab/ci/status/factory.rb
index 3446644eff8..2a0bf060c9b 100644
--- a/lib/gitlab/ci/status/factory.rb
+++ b/lib/gitlab/ci/status/factory.rb
@@ -34,11 +34,9 @@ module Gitlab
def extended_statuses
return @extended_statuses if defined?(@extended_statuses)
- groups = self.class.extended_statuses.map do |group|
+ @extended_statuses = self.class.extended_statuses.flat_map do |group|
Array(group).find { |status| status.matches?(@subject, @user) }
- end
-
- @extended_statuses = groups.flatten.compact
+ end.compact
end
def self.extended_statuses
diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
index cf3d261c1cb..5c1c0c142e5 100644
--- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml
@@ -50,13 +50,12 @@ variables:
POSTGRES_DB: $CI_ENVIRONMENT_SLUG
POSTGRES_VERSION: 9.6.2
- KUBERNETES_VERSION: 1.11.10
- HELM_VERSION: 2.14.0
-
DOCKER_DRIVER: overlay2
ROLLOUT_RESOURCE_TYPE: deployment
+ DOCKER_TLS_CERTDIR: "" # https://gitlab.com/gitlab-org/gitlab-runner/issues/4501
+
stages:
- build
- test
diff --git a/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml
index a09217e8cf0..b0a79950667 100644
--- a/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Browser-Performance-Testing.gitlab-ci.yml
@@ -2,6 +2,8 @@ performance:
stage: performance
image: docker:stable
allow_failure: true
+ variables:
+ DOCKER_TLS_CERTDIR: ""
services:
- docker:stable-dind
script:
diff --git a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
index 18f7290e1d9..8061da968ed 100644
--- a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml
@@ -1,6 +1,8 @@
build:
stage: build
image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-build-image/master:stable"
+ variables:
+ DOCKER_TLS_CERTDIR: ""
services:
- docker:stable-dind
script:
diff --git a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
index 8a84744aa2d..3adc6a72874 100644
--- a/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Code-Quality.gitlab-ci.yml
@@ -6,6 +6,7 @@ code_quality:
- docker:stable-dind
variables:
DOCKER_DRIVER: overlay2
+ DOCKER_TLS_CERTDIR: ""
script:
- |
if ! docker info &>/dev/null; then
@@ -22,6 +23,7 @@ code_quality:
reports:
codequality: gl-code-quality-report.json
expire_in: 1 week
+ dependencies: []
only:
- branches
- tags
diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
index 108f0119ae1..a8ec2d4781d 100644
--- a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
@@ -1,14 +1,17 @@
+.auto-deploy:
+ image: "registry.gitlab.com/gitlab-org/cluster-integration/auto-deploy-image:v0.1.0"
+
review:
+ extends: .auto-deploy
stage: review
script:
- - check_kube_domain
- - install_dependencies
- - download_chart
- - ensure_namespace
- - initialize_tiller
- - create_secret
- - deploy
- - persist_environment_url
+ - auto-deploy check_kube_domain
+ - auto-deploy download_chart
+ - auto-deploy ensure_namespace
+ - auto-deploy initialize_tiller
+ - auto-deploy create_secret
+ - auto-deploy deploy
+ - auto-deploy persist_environment_url
environment:
name: review/$CI_COMMIT_REF_NAME
url: http://$CI_PROJECT_ID-$CI_ENVIRONMENT_SLUG.$KUBE_INGRESS_BASE_DOMAIN
@@ -27,13 +30,13 @@ review:
- $REVIEW_DISABLED
stop_review:
+ extends: .auto-deploy
stage: cleanup
variables:
GIT_STRATEGY: none
script:
- - install_dependencies
- - initialize_tiller
- - delete
+ - auto-deploy initialize_tiller
+ - auto-deploy delete
environment:
name: review/$CI_COMMIT_REF_NAME
action: stop
@@ -57,15 +60,15 @@ stop_review:
# STAGING_ENABLED.
staging:
+ extends: .auto-deploy
stage: staging
script:
- - check_kube_domain
- - install_dependencies
- - download_chart
- - ensure_namespace
- - initialize_tiller
- - create_secret
- - deploy
+ - auto-deploy check_kube_domain
+ - auto-deploy download_chart
+ - auto-deploy ensure_namespace
+ - auto-deploy initialize_tiller
+ - auto-deploy create_secret
+ - auto-deploy deploy
environment:
name: staging
url: http://$CI_PROJECT_PATH_SLUG-staging.$KUBE_INGRESS_BASE_DOMAIN
@@ -81,15 +84,15 @@ staging:
# CANARY_ENABLED.
canary:
+ extends: .auto-deploy
stage: canary
script:
- - check_kube_domain
- - install_dependencies
- - download_chart
- - ensure_namespace
- - initialize_tiller
- - create_secret
- - deploy canary
+ - auto-deploy check_kube_domain
+ - auto-deploy download_chart
+ - auto-deploy ensure_namespace
+ - auto-deploy initialize_tiller
+ - auto-deploy create_secret
+ - auto-deploy deploy canary
environment:
name: production
url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN
@@ -102,18 +105,18 @@ canary:
- $CANARY_ENABLED
.production: &production_template
+ extends: .auto-deploy
stage: production
script:
- - check_kube_domain
- - install_dependencies
- - download_chart
- - ensure_namespace
- - initialize_tiller
- - create_secret
- - deploy
- - delete canary
- - delete rollout
- - persist_environment_url
+ - auto-deploy check_kube_domain
+ - auto-deploy download_chart
+ - auto-deploy ensure_namespace
+ - auto-deploy initialize_tiller
+ - auto-deploy create_secret
+ - auto-deploy deploy
+ - auto-deploy delete canary
+ - auto-deploy delete rollout
+ - auto-deploy persist_environment_url
environment:
name: production
url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN
@@ -152,17 +155,17 @@ production_manual:
# This job implements incremental rollout on for every push to `master`.
.rollout: &rollout_template
+ extends: .auto-deploy
script:
- - check_kube_domain
- - install_dependencies
- - download_chart
- - ensure_namespace
- - initialize_tiller
- - create_secret
- - deploy rollout $ROLLOUT_PERCENTAGE
- - scale stable $((100-ROLLOUT_PERCENTAGE))
- - delete canary
- - persist_environment_url
+ - auto-deploy check_kube_domain
+ - auto-deploy download_chart
+ - auto-deploy ensure_namespace
+ - auto-deploy initialize_tiller
+ - auto-deploy create_secret
+ - auto-deploy deploy rollout $ROLLOUT_PERCENTAGE
+ - auto-deploy scale stable $((100-ROLLOUT_PERCENTAGE))
+ - auto-deploy delete canary
+ - auto-deploy persist_environment_url
environment:
name: production
url: http://$CI_PROJECT_PATH_SLUG.$KUBE_INGRESS_BASE_DOMAIN
@@ -240,330 +243,3 @@ rollout 100%:
<<: *manual_rollout_template
<<: *production_template
allow_failure: false
-
-.deploy_helpers: &deploy_helpers |
- [[ "$TRACE" ]] && set -x
- auto_database_url=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@${CI_ENVIRONMENT_SLUG}-postgres:5432/${POSTGRES_DB}
- export DATABASE_URL=${DATABASE_URL-$auto_database_url}
- export TILLER_NAMESPACE=$KUBE_NAMESPACE
-
- function get_replicas() {
- track="${1:-stable}"
- percentage="${2:-100}"
-
- env_track=$( echo $track | tr -s '[:lower:]' '[:upper:]' )
- env_slug=$( echo ${CI_ENVIRONMENT_SLUG//-/_} | tr -s '[:lower:]' '[:upper:]' )
-
- if [[ "$track" == "stable" ]] || [[ "$track" == "rollout" ]]; then
- # for stable track get number of replicas from `PRODUCTION_REPLICAS`
- eval new_replicas=\$${env_slug}_REPLICAS
- if [[ -z "$new_replicas" ]]; then
- new_replicas=$REPLICAS
- fi
- else
- # for all tracks get number of replicas from `CANARY_PRODUCTION_REPLICAS`
- eval new_replicas=\$${env_track}_${env_slug}_REPLICAS
- if [[ -z "$new_replicas" ]]; then
- eval new_replicas=\${env_track}_REPLICAS
- fi
- fi
-
- replicas="${new_replicas:-1}"
- replicas="$(($replicas * $percentage / 100))"
-
- # always return at least one replicas
- if [[ $replicas -gt 0 ]]; then
- echo "$replicas"
- else
- echo 1
- fi
- }
-
- # Extracts variables prefixed with K8S_SECRET_
- # and creates a Kubernetes secret.
- #
- # e.g. If we have the following environment variables:
- # K8S_SECRET_A=value1
- # K8S_SECRET_B=multi\ word\ value
- #
- # Then we will create a secret with the following key-value pairs:
- # data:
- # A: dmFsdWUxCg==
- # B: bXVsdGkgd29yZCB2YWx1ZQo=
- function create_application_secret() {
- track="${1-stable}"
- export APPLICATION_SECRET_NAME=$(application_secret_name "$track")
-
- env | sed -n "s/^K8S_SECRET_\(.*\)$/\1/p" > k8s_prefixed_variables
-
- kubectl create secret \
- -n "$KUBE_NAMESPACE" generic "$APPLICATION_SECRET_NAME" \
- --from-env-file k8s_prefixed_variables -o yaml --dry-run |
- kubectl replace -n "$KUBE_NAMESPACE" --force -f -
-
- export APPLICATION_SECRET_CHECKSUM=$(cat k8s_prefixed_variables | sha256sum | cut -d ' ' -f 1)
-
- rm k8s_prefixed_variables
- }
-
- function deploy_name() {
- name="$CI_ENVIRONMENT_SLUG"
- track="${1-stable}"
-
- if [[ "$track" != "stable" ]]; then
- name="$name-$track"
- fi
-
- echo $name
- }
-
- function application_secret_name() {
- track="${1-stable}"
- name=$(deploy_name "$track")
-
- echo "${name}-secret"
- }
-
- function deploy() {
- track="${1-stable}"
- percentage="${2:-100}"
- name=$(deploy_name "$track")
-
- if [[ -z "$CI_COMMIT_TAG" ]]; then
- image_repository=${CI_APPLICATION_REPOSITORY:-$CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG}
- image_tag=${CI_APPLICATION_TAG:-$CI_COMMIT_SHA}
- else
- image_repository=${CI_APPLICATION_REPOSITORY:-$CI_REGISTRY_IMAGE}
- image_tag=${CI_APPLICATION_TAG:-$CI_COMMIT_TAG}
- fi
-
- service_enabled="true"
- postgres_enabled="$POSTGRES_ENABLED"
-
- # if track is different than stable,
- # re-use all attached resources
- if [[ "$track" != "stable" ]]; then
- service_enabled="false"
- postgres_enabled="false"
- fi
-
- replicas=$(get_replicas "$track" "$percentage")
-
- if [[ "$CI_PROJECT_VISIBILITY" != "public" ]]; then
- secret_name='gitlab-registry'
- else
- secret_name=''
- fi
-
- create_application_secret "$track"
-
- env_slug=$(echo ${CI_ENVIRONMENT_SLUG//-/_} | tr -s '[:lower:]' '[:upper:]')
- eval env_ADDITIONAL_HOSTS=\$${env_slug}_ADDITIONAL_HOSTS
- if [ -n "$env_ADDITIONAL_HOSTS" ]; then
- additional_hosts="{$env_ADDITIONAL_HOSTS}"
- elif [ -n "$ADDITIONAL_HOSTS" ]; then
- additional_hosts="{$ADDITIONAL_HOSTS}"
- fi
-
- if [[ -n "$DB_INITIALIZE" && -z "$(helm ls -q "^$name$")" ]]; then
- echo "Deploying first release with database initialization..."
- helm upgrade --install \
- --wait \
- --set service.enabled="$service_enabled" \
- --set gitlab.app="$CI_PROJECT_PATH_SLUG" \
- --set gitlab.env="$CI_ENVIRONMENT_SLUG" \
- --set releaseOverride="$CI_ENVIRONMENT_SLUG" \
- --set image.repository="$image_repository" \
- --set image.tag="$image_tag" \
- --set image.pullPolicy=IfNotPresent \
- --set image.secrets[0].name="$secret_name" \
- --set application.track="$track" \
- --set application.database_url="$DATABASE_URL" \
- --set application.secretName="$APPLICATION_SECRET_NAME" \
- --set application.secretChecksum="$APPLICATION_SECRET_CHECKSUM" \
- --set service.commonName="le-$CI_PROJECT_ID.$KUBE_INGRESS_BASE_DOMAIN" \
- --set service.url="$CI_ENVIRONMENT_URL" \
- --set service.additionalHosts="$additional_hosts" \
- --set replicaCount="$replicas" \
- --set postgresql.enabled="$postgres_enabled" \
- --set postgresql.nameOverride="postgres" \
- --set postgresql.postgresUser="$POSTGRES_USER" \
- --set postgresql.postgresPassword="$POSTGRES_PASSWORD" \
- --set postgresql.postgresDatabase="$POSTGRES_DB" \
- --set postgresql.imageTag="$POSTGRES_VERSION" \
- --set application.initializeCommand="$DB_INITIALIZE" \
- $HELM_UPGRADE_EXTRA_ARGS \
- --namespace="$KUBE_NAMESPACE" \
- "$name" \
- chart/
-
- echo "Deploying second release..."
- helm upgrade --reuse-values \
- --wait \
- --set application.initializeCommand="" \
- --set application.migrateCommand="$DB_MIGRATE" \
- $HELM_UPGRADE_EXTRA_ARGS \
- --namespace="$KUBE_NAMESPACE" \
- "$name" \
- chart/
- else
- echo "Deploying new release..."
- helm upgrade --install \
- --wait \
- --set service.enabled="$service_enabled" \
- --set gitlab.app="$CI_PROJECT_PATH_SLUG" \
- --set gitlab.env="$CI_ENVIRONMENT_SLUG" \
- --set releaseOverride="$CI_ENVIRONMENT_SLUG" \
- --set image.repository="$image_repository" \
- --set image.tag="$image_tag" \
- --set image.pullPolicy=IfNotPresent \
- --set image.secrets[0].name="$secret_name" \
- --set application.track="$track" \
- --set application.database_url="$DATABASE_URL" \
- --set application.secretName="$APPLICATION_SECRET_NAME" \
- --set application.secretChecksum="$APPLICATION_SECRET_CHECKSUM" \
- --set service.commonName="le-$CI_PROJECT_ID.$KUBE_INGRESS_BASE_DOMAIN" \
- --set service.url="$CI_ENVIRONMENT_URL" \
- --set service.additionalHosts="$additional_hosts" \
- --set replicaCount="$replicas" \
- --set postgresql.enabled="$postgres_enabled" \
- --set postgresql.nameOverride="postgres" \
- --set postgresql.postgresUser="$POSTGRES_USER" \
- --set postgresql.postgresPassword="$POSTGRES_PASSWORD" \
- --set postgresql.postgresDatabase="$POSTGRES_DB" \
- --set postgresql.imageTag="$POSTGRES_VERSION" \
- --set application.migrateCommand="$DB_MIGRATE" \
- $HELM_UPGRADE_EXTRA_ARGS \
- --namespace="$KUBE_NAMESPACE" \
- "$name" \
- chart/
- fi
-
- if [[ -z "$ROLLOUT_STATUS_DISABLED" ]]; then
- kubectl rollout status -n "$KUBE_NAMESPACE" -w "$ROLLOUT_RESOURCE_TYPE/$name"
- fi
- }
-
- function scale() {
- track="${1-stable}"
- percentage="${2-100}"
- name=$(deploy_name "$track")
-
- replicas=$(get_replicas "$track" "$percentage")
-
- if [[ -n "$(helm ls -q "^$name$")" ]]; then
- helm upgrade --reuse-values \
- --wait \
- --set replicaCount="$replicas" \
- --namespace="$KUBE_NAMESPACE" \
- "$name" \
- chart/
- fi
- }
-
- function install_dependencies() {
- apk add -U openssl curl tar gzip bash ca-certificates git
- curl -sSL -o /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub
- curl -sSL -O https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.28-r0/glibc-2.28-r0.apk
- apk add glibc-2.28-r0.apk
- rm glibc-2.28-r0.apk
-
- curl -sS "https://kubernetes-helm.storage.googleapis.com/helm-v${HELM_VERSION}-linux-amd64.tar.gz" | tar zx
- mv linux-amd64/helm /usr/bin/
- mv linux-amd64/tiller /usr/bin/
- helm version --client
- tiller -version
-
- curl -sSL -o /usr/bin/kubectl "https://storage.googleapis.com/kubernetes-release/release/v${KUBERNETES_VERSION}/bin/linux/amd64/kubectl"
- chmod +x /usr/bin/kubectl
- kubectl version --client
- }
-
- function download_chart() {
- if [[ ! -d chart ]]; then
- auto_chart=${AUTO_DEVOPS_CHART:-gitlab/auto-deploy-app}
- auto_chart_name=$(basename $auto_chart)
- auto_chart_name=${auto_chart_name%.tgz}
- auto_chart_name=${auto_chart_name%.tar.gz}
- else
- auto_chart="chart"
- auto_chart_name="chart"
- fi
-
- helm init --client-only
- helm repo add ${AUTO_DEVOPS_CHART_REPOSITORY_NAME:-gitlab} ${AUTO_DEVOPS_CHART_REPOSITORY:-https://charts.gitlab.io} ${AUTO_DEVOPS_CHART_REPOSITORY_USERNAME:+"--username" "$AUTO_DEVOPS_CHART_REPOSITORY_USERNAME"} ${AUTO_DEVOPS_CHART_REPOSITORY_PASSWORD:+"--password" "$AUTO_DEVOPS_CHART_REPOSITORY_PASSWORD"}
- if [[ ! -d "$auto_chart" ]]; then
- helm fetch ${auto_chart} --untar
- fi
- if [ "$auto_chart_name" != "chart" ]; then
- mv ${auto_chart_name} chart
- fi
-
- helm dependency update chart/
- helm dependency build chart/
- }
-
- function ensure_namespace() {
- kubectl get namespace "$KUBE_NAMESPACE" || kubectl create namespace "$KUBE_NAMESPACE"
- }
-
- function check_kube_domain() {
- if [[ -z "$KUBE_INGRESS_BASE_DOMAIN" ]]; then
- echo "In order to deploy or use Review Apps,"
- echo "KUBE_INGRESS_BASE_DOMAIN variables must be set"
- echo "From 11.8, you can set KUBE_INGRESS_BASE_DOMAIN in cluster settings"
- echo "or by defining a variable at group or project level."
- echo "You can also manually add it in .gitlab-ci.yml"
- false
- else
- true
- fi
- }
-
- function initialize_tiller() {
- echo "Checking Tiller..."
-
- export HELM_HOST="localhost:44134"
- tiller -listen ${HELM_HOST} -alsologtostderr > /dev/null 2>&1 &
- echo "Tiller is listening on ${HELM_HOST}"
-
- if ! helm version --debug; then
- echo "Failed to init Tiller."
- return 1
- fi
- echo ""
- }
-
- function create_secret() {
- echo "Create secret..."
- if [[ "$CI_PROJECT_VISIBILITY" == "public" ]]; then
- return
- fi
-
- kubectl create secret -n "$KUBE_NAMESPACE" \
- docker-registry gitlab-registry \
- --docker-server="$CI_REGISTRY" \
- --docker-username="${CI_DEPLOY_USER:-$CI_REGISTRY_USER}" \
- --docker-password="${CI_DEPLOY_PASSWORD:-$CI_REGISTRY_PASSWORD}" \
- --docker-email="$GITLAB_USER_EMAIL" \
- -o yaml --dry-run | kubectl replace -n "$KUBE_NAMESPACE" --force -f -
- }
-
- function persist_environment_url() {
- echo $CI_ENVIRONMENT_URL > environment_url.txt
- }
-
- function delete() {
- track="${1-stable}"
- name=$(deploy_name "$track")
-
- if [[ -n "$(helm ls -q "^$name$")" ]]; then
- helm delete --purge "$name"
- fi
-
- secret_name=$(application_secret_name "$track")
- kubectl delete secret --ignore-not-found -n "$KUBE_NAMESPACE" "$secret_name"
- }
-
-before_script:
- - *deploy_helpers
diff --git a/lib/gitlab/ci/templates/Maven.gitlab-ci.yml b/lib/gitlab/ci/templates/Maven.gitlab-ci.yml
index 13ab98d3a16..84bb0ff3b33 100644
--- a/lib/gitlab/ci/templates/Maven.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Maven.gitlab-ci.yml
@@ -1,5 +1,3 @@
-# This file is a template, and might need editing before it works on your project.
-
# Build JAVA applications using Apache Maven (http://maven.apache.org)
# For docker image tags see https://hub.docker.com/_/maven/
#
diff --git a/lib/gitlab/ci/templates/Packer.gitlab-ci.yml b/lib/gitlab/ci/templates/Packer.gitlab-ci.yml
index 83e179f37c3..0a3cf3dcf77 100644
--- a/lib/gitlab/ci/templates/Packer.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Packer.gitlab-ci.yml
@@ -1,5 +1,5 @@
image:
- name: hashicorp/packer:1.0.4
+ name: hashicorp/packer:latest
entrypoint:
- '/usr/bin/env'
- 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'
diff --git a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
index 5ad624bb15f..2afc99d0bf8 100644
--- a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
@@ -5,6 +5,7 @@ container_scanning:
image: docker:stable
variables:
DOCKER_DRIVER: overlay2
+ DOCKER_TLS_CERTDIR: ""
# Defining two new variables based on GitLab's CI/CD predefined variables
# https://docs.gitlab.com/ee/ci/variables/#predefined-environment-variables
CI_APPLICATION_REPOSITORY: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG
@@ -22,8 +23,9 @@ container_scanning:
DOCKER_SERVICE: docker
DOCKER_HOST: tcp://${DOCKER_SERVICE}:2375/
# https://hub.docker.com/r/arminc/clair-local-scan/tags
- CLAIR_LOCAL_SCAN_VERSION: v2.0.8_fe9b059d930314b54c78f75afe265955faf4fdc1
- CLAIR_EXECUTABLE_VERSION: v11
+ CLAIR_LOCAL_SCAN_VERSION: v2.0.8_0ed98e9ead65a51ba53f7cc53fa5e80c92169207
+ CLAIR_EXECUTABLE_VERSION: v12
+ CLAIR_EXECUTABLE_SHA: 44f2a3fdd7b0d102c98510e7586f6956edc89ab72c6943980f92f4979f7f4081
## Disable the proxy for clair-local-scan, otherwise Container Scanning will
## fail when a proxy is used.
NO_PROXY: ${DOCKER_SERVICE},localhost
@@ -43,6 +45,7 @@ container_scanning:
- apk add -U wget ca-certificates
- docker pull ${CI_APPLICATION_REPOSITORY}:${CI_APPLICATION_TAG}
- wget https://github.com/arminc/clair-scanner/releases/download/${CLAIR_EXECUTABLE_VERSION}/clair-scanner_linux_amd64
+ - echo "${CLAIR_EXECUTABLE_SHA} clair-scanner_linux_amd64" | sha256sum -c
- mv clair-scanner_linux_amd64 clair-scanner
- chmod +x clair-scanner
- touch clair-whitelist.yml
diff --git a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
index f176771775e..15b84f1540d 100644
--- a/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Dependency-Scanning.gitlab-ci.yml
@@ -9,6 +9,7 @@ dependency_scanning:
image: docker:stable
variables:
DOCKER_DRIVER: overlay2
+ DOCKER_TLS_CERTDIR: ""
allow_failure: true
services:
- docker:stable-dind
@@ -41,6 +42,9 @@ dependency_scanning:
DS_PULL_ANALYZER_IMAGE_TIMEOUT \
DS_RUN_ANALYZER_TIMEOUT \
DS_PYTHON_VERSION \
+ DS_PIP_DEPENDENCY_PATH \
+ PIP_INDEX_URL \
+ PIP_EXTRA_INDEX_URL \
) \
--volume "$PWD:/code" \
--volume /var/run/docker.sock:/var/run/docker.sock \
diff --git a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
index 0a97a16b83c..90278122361 100644
--- a/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/SAST.gitlab-ci.yml
@@ -9,6 +9,7 @@ sast:
image: docker:stable
variables:
DOCKER_DRIVER: overlay2
+ DOCKER_TLS_CERTDIR: ""
allow_failure: true
services:
- docker:stable-dind
@@ -45,11 +46,14 @@ sast:
SAST_DOCKER_CLIENT_NEGOTIATION_TIMEOUT \
SAST_PULL_ANALYZER_IMAGE_TIMEOUT \
SAST_RUN_ANALYZER_TIMEOUT \
+ SAST_JAVA_VERSION \
ANT_HOME \
ANT_PATH \
GRADLE_PATH \
JAVA_OPTS \
JAVA_PATH \
+ JAVA_8_VERSION \
+ JAVA_11_VERSION \
MAVEN_CLI_OPTS \
MAVEN_PATH \
MAVEN_REPO_PATH \
diff --git a/lib/gitlab/ci/templates/Serverless.gitlab-ci.yml b/lib/gitlab/ci/templates/Serverless.gitlab-ci.yml
index a3db2705bf6..280e75d46f5 100644
--- a/lib/gitlab/ci/templates/Serverless.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Serverless.gitlab-ci.yml
@@ -8,26 +8,23 @@ stages:
- deploy
.serverless:build:image:
- stage: build
image: registry.gitlab.com/gitlab-org/gitlabktl:latest
+ stage: build
script: /usr/bin/gitlabktl app build
.serverless:deploy:image:
+ image: registry.gitlab.com/gitlab-org/gitlabktl:latest
stage: deploy
- image: gcr.io/triggermesh/tm@sha256:3cfdd470a66b741004fb02354319d79f1598c70117ce79978d2e07e192bfb336 # v0.0.11
environment: development
- script:
- - echo "$CI_REGISTRY_IMAGE"
- - tm -n "$KUBE_NAMESPACE" --config "$KUBECONFIG" deploy service "$CI_PROJECT_NAME" --from-image "$CI_REGISTRY_IMAGE" --wait
+ script: /usr/bin/gitlabktl app deploy
.serverless:build:functions:
- stage: build
- environment: development
image: registry.gitlab.com/gitlab-org/gitlabktl:latest
+ stage: build
script: /usr/bin/gitlabktl serverless build
.serverless:deploy:functions:
+ image: registry.gitlab.com/gitlab-org/gitlabktl:latest
stage: deploy
environment: development
- image: registry.gitlab.com/gitlab-org/gitlabktl:latest
script: /usr/bin/gitlabktl serverless deploy
diff --git a/lib/gitlab/ci/trace.rb b/lib/gitlab/ci/trace.rb
index ce5857965bf..9550bc6d39c 100644
--- a/lib/gitlab/ci/trace.rb
+++ b/lib/gitlab/ci/trace.rb
@@ -63,7 +63,15 @@ module Gitlab
end
def exist?
- trace_artifact&.exists? || job.trace_chunks.any? || current_path.present? || old_trace.present?
+ archived_trace_exist? || live_trace_exist?
+ end
+
+ def archived_trace_exist?
+ trace_artifact&.exists?
+ end
+
+ def live_trace_exist?
+ job.trace_chunks.any? || current_path.present? || old_trace.present?
end
def read
@@ -118,7 +126,7 @@ module Gitlab
raise AlreadyArchivedError, 'Could not write to the archived trace'
elsif current_path
File.open(current_path, mode)
- elsif Feature.enabled?('ci_enable_live_trace')
+ elsif Feature.enabled?('ci_enable_live_trace', job.project)
Gitlab::Ci::Trace::ChunkedIO.new(job)
else
File.open(ensure_path, mode)
@@ -167,7 +175,7 @@ module Gitlab
def clone_file!(src_stream, temp_dir)
FileUtils.mkdir_p(temp_dir)
- Dir.mktmpdir('tmp-trace', temp_dir) do |dir_path|
+ Dir.mktmpdir("tmp-trace-#{job.id}", temp_dir) do |dir_path|
temp_path = File.join(dir_path, "job.log")
FileUtils.touch(temp_path)
size = IO.copy_stream(src_stream, temp_path)
diff --git a/lib/gitlab/ci/trace/chunked_io.rb b/lib/gitlab/ci/trace/chunked_io.rb
index 8c6fd56493f..e99889f4a25 100644
--- a/lib/gitlab/ci/trace/chunked_io.rb
+++ b/lib/gitlab/ci/trace/chunked_io.rb
@@ -166,6 +166,13 @@ module Gitlab
end
def destroy!
+ # TODO: Remove this logging once we confirmed new live trace architecture is functional.
+ # See https://gitlab.com/gitlab-com/gl-infra/infrastructure/issues/4667.
+ unless build.has_archived_trace?
+ Sidekiq.logger.warn(message: 'The job does not have archived trace but going to be destroyed.',
+ job_id: build.id)
+ end
+
trace_chunks.fast_destroy_all
@tell = @size = 0
ensure
diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb
index a5693dc4f81..2e1eab270ff 100644
--- a/lib/gitlab/ci/yaml_processor.rb
+++ b/lib/gitlab/ci/yaml_processor.rb
@@ -40,6 +40,7 @@ module Gitlab
environment: job[:environment_name],
coverage_regex: job[:coverage],
yaml_variables: yaml_variables(name),
+ needs_attributes: job[:needs]&.map { |need| { name: need } },
options: {
image: job[:image],
services: job[:services],
@@ -54,7 +55,8 @@ module Gitlab
parallel: job[:parallel],
instance: job[:instance],
start_in: job[:start_in],
- trigger: job[:trigger]
+ trigger: job[:trigger],
+ bridge_needs: job[:needs]
}.compact }.compact
end
@@ -108,6 +110,7 @@ module Gitlab
validate_job_stage!(name, job)
validate_job_dependencies!(name, job)
+ validate_job_needs!(name, job)
validate_job_environment!(name, job)
end
end
@@ -144,12 +147,30 @@ module Gitlab
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
+ dependency_stage_index = @stages.index(@jobs[dependency.to_sym][:stage])
+
+ unless dependency_stage_index.present? && dependency_stage_index < stage_index
raise ValidationError, "#{name} job: dependency #{dependency} is not defined in prior stages"
end
end
end
+ def validate_job_needs!(name, job)
+ return unless job[:needs]
+
+ stage_index = @stages.index(job[:stage])
+
+ job[:needs].each do |need|
+ raise ValidationError, "#{name} job: undefined need: #{need}" unless @jobs[need.to_sym]
+
+ needs_stage_index = @stages.index(@jobs[need.to_sym][:stage])
+
+ unless needs_stage_index.present? && needs_stage_index < stage_index
+ raise ValidationError, "#{name} job: need #{need} 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)