diff options
Diffstat (limited to 'lib/gitlab/ci')
42 files changed, 293 insertions, 152 deletions
diff --git a/lib/gitlab/ci/ansi2html.rb b/lib/gitlab/ci/ansi2html.rb index 42a8b561d34..d581fbfda42 100644 --- a/lib/gitlab/ci/ansi2html.rb +++ b/lib/gitlab/ci/ansi2html.rb @@ -299,7 +299,7 @@ module Gitlab end def handle_new_line - write_in_tag %{<br/>} + write_in_tag %(<br/>) close_open_tags if @sections.any? && @lineno_in_section == 0 @lineno_in_section += 1 @@ -324,7 +324,7 @@ module Gitlab return if @sections.include?(section) @sections << section - write_raw %{<div class="section-start" data-timestamp="#{timestamp}" data-section="#{data_section_names}" role="button"></div>} + write_raw %(<div class="section-start" data-timestamp="#{timestamp}" data-section="#{data_section_names}" role="button"></div>) @lineno_in_section = 0 end @@ -333,7 +333,7 @@ module Gitlab # close all sections up to section until @sections.empty? - write_raw %{<div class="section-end" data-section="#{data_section_names}"></div>} + write_raw %(<div class="section-end" data-section="#{data_section_names}"></div>) last_section = @sections.pop break if section == last_section @@ -423,9 +423,9 @@ module Gitlab close_open_tags @out << if css_classes.any? - %{<span class="#{css_classes.join(' ')}">} + %(<span class="#{css_classes.join(' ')}">) else - %{<span>} + %(<span>) end @n_open_tags += 1 @@ -433,7 +433,7 @@ module Gitlab def close_open_tags while @n_open_tags > 0 - @out << %{</span>} + @out << %(</span>) @n_open_tags -= 1 end end diff --git a/lib/gitlab/ci/build/artifacts/metadata.rb b/lib/gitlab/ci/build/artifacts/metadata.rb index d0ab4916c90..5748b8e34cf 100644 --- a/lib/gitlab/ci/build/artifacts/metadata.rb +++ b/lib/gitlab/ci/build/artifacts/metadata.rb @@ -18,7 +18,11 @@ module Gitlab def initialize(stream, path, **opts) @stream = stream - @path = path + + # Ensure to remove any ./ prefix from the path + # so that the pattern matching will work as expected + @path = path.gsub(%r{^\./}, '') + @opts = opts @full_version = read_version end @@ -59,7 +63,7 @@ module Gitlab entries = {} child_pattern = '[^/]*/?$' unless @opts[:recursive] - match_pattern = /^#{Regexp.escape(@path)}#{child_pattern}/ + match_pattern = /^#{Regexp.escape(path)}#{child_pattern}/ until gz.eof? begin diff --git a/lib/gitlab/ci/build/duration_parser.rb b/lib/gitlab/ci/build/duration_parser.rb index 9385dccd5f3..97049a4f876 100644 --- a/lib/gitlab/ci/build/duration_parser.rb +++ b/lib/gitlab/ci/build/duration_parser.rb @@ -41,7 +41,7 @@ module Gitlab def parse return if never? - ChronicDuration.parse(value) + ChronicDuration.parse(value, use_complete_matcher: true) end def validation_cache diff --git a/lib/gitlab/ci/components/instance_path.rb b/lib/gitlab/ci/components/instance_path.rb index e0ef598da1b..17c784c4d54 100644 --- a/lib/gitlab/ci/components/instance_path.rb +++ b/lib/gitlab/ci/components/instance_path.rb @@ -7,17 +7,19 @@ module Gitlab include Gitlab::Utils::StrongMemoize LATEST_VERSION_KEYWORD = '~latest' + TEMPLATES_DIR = 'templates' def self.match?(address) address.include?('@') && address.start_with?(Settings.gitlab_ci['component_fqdn']) end - attr_reader :host + attr_reader :host, :project_file_path def initialize(address:, content_filename:) @full_path, @version = address.to_s.split('@', 2) @content_filename = content_filename @host = Settings.gitlab_ci['component_fqdn'] + @project_file_path = nil end def fetch_content!(current_user:) @@ -26,7 +28,7 @@ module Gitlab raise Gitlab::Access::AccessDeniedError unless Ability.allowed?(current_user, :download_code, project) - project.repository.blob_data_at(sha, project_file_path) + content(simple_template_path) || content(complex_template_path) || content(legacy_template_path) end def project @@ -34,13 +36,6 @@ module Gitlab end strong_memoize_attr :project - def project_file_path - return unless project - - component_dir = instance_path.delete_prefix(project.full_path) - File.join(component_dir, @content_filename).delete_prefix('/') - end - def sha return unless project return latest_version_sha if version == LATEST_VERSION_KEYWORD @@ -57,6 +52,11 @@ module Gitlab @full_path.delete_prefix(host) end + def component_path + instance_path.delete_prefix(project.full_path).delete_prefix('/') + end + strong_memoize_attr :component_path + # Given a path like "my-org/sub-group/the-project/path/to/component" # find the project "my-org/sub-group/the-project" by looking at all possible paths. def find_project_by_component_path(path) @@ -75,6 +75,36 @@ module Gitlab def latest_version_sha project.releases.latest&.sha end + + # A simple template consists of a single file + def simple_template_path + # Extract this line and move to fetch_content once we remove legacy fetching + return unless templates_dir_exists? && component_path.index('/').nil? + + @project_file_path = File.join(TEMPLATES_DIR, "#{component_path}.yml") + end + + # A complex template is directory-based and may consist of multiple files. + # Given a path like "my-org/sub-group/the-project/templates/component" + # returns the entry point path: "templates/component/template.yml". + def complex_template_path + # Extract this line and move to fetch_content once we remove legacy fetching + return unless templates_dir_exists? && component_path.index('/').nil? + + @project_file_path = File.join(TEMPLATES_DIR, component_path, @content_filename) + end + + def legacy_template_path + @project_file_path = File.join(component_path, @content_filename).delete_prefix('/') + end + + def templates_dir_exists? + project.repository.tree.trees.map(&:name).include?(TEMPLATES_DIR) + end + + def content(path) + project.repository.blob_data_at(sha, path) + end end end end diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb index 0c293c3f0ef..73d329930a5 100644 --- a/lib/gitlab/ci/config.rb +++ b/lib/gitlab/ci/config.rb @@ -161,6 +161,7 @@ module Gitlab def build_context(project:, pipeline:, sha:, user:, parent_pipeline:, pipeline_config:) Config::External::Context.new( project: project, + pipeline: pipeline, sha: sha || find_sha(project), user: user, parent_pipeline: parent_pipeline, diff --git a/lib/gitlab/ci/config/entry/bridge.rb b/lib/gitlab/ci/config/entry/bridge.rb index ee99354cb28..1119afab24a 100644 --- a/lib/gitlab/ci/config/entry/bridge.rb +++ b/lib/gitlab/ci/config/entry/bridge.rb @@ -51,7 +51,7 @@ module Gitlab entry :parallel, Entry::Product::Parallel, description: 'Parallel configuration for this job.', inherit: false, - metadata: { allowed_strategies: %i(matrix) } + metadata: { allowed_strategies: %i[matrix] } attributes :when, :allow_failure, :parallel diff --git a/lib/gitlab/ci/config/entry/default.rb b/lib/gitlab/ci/config/entry/default.rb index e996b6b1312..476b928e471 100644 --- a/lib/gitlab/ci/config/entry/default.rb +++ b/lib/gitlab/ci/config/entry/default.rb @@ -14,7 +14,7 @@ module Gitlab include ::Gitlab::Config::Entry::Inheritable ALLOWED_KEYS = %i[before_script after_script hooks cache image services - interruptible timeout retry tags artifacts].freeze + interruptible timeout retry tags artifacts id_tokens].freeze validations do validates :config, allowed_keys: ALLOWED_KEYS @@ -65,6 +65,11 @@ module Gitlab description: 'Default artifacts.', inherit: false + entry :id_tokens, ::Gitlab::Config::Entry::ComposableHash, + description: 'Configured JWTs for this job', + inherit: false, + metadata: { composable_class: ::Gitlab::Ci::Config::Entry::IdToken } + private def overwrite_entry(deps, key, current_entry) diff --git a/lib/gitlab/ci/config/entry/include/rules.rb b/lib/gitlab/ci/config/entry/include/rules.rb index 71418e6752d..a3799b36ece 100644 --- a/lib/gitlab/ci/config/entry/include/rules.rb +++ b/lib/gitlab/ci/config/entry/include/rules.rb @@ -19,11 +19,6 @@ module Gitlab validates :config, type: Array end - # Remove this method when FF `ci_refactor_external_rules` is removed - def value - Feature.enabled?(:ci_refactor_external_rules) ? super : @config - end - def composable_class Entry::Include::Rules::Rule end diff --git a/lib/gitlab/ci/config/entry/include/rules/rule.rb b/lib/gitlab/ci/config/entry/include/rules/rule.rb index 1a68e95913c..df8509eecc0 100644 --- a/lib/gitlab/ci/config/entry/include/rules/rule.rb +++ b/lib/gitlab/ci/config/entry/include/rules/rule.rb @@ -7,13 +7,17 @@ module Gitlab class Include class Rules::Rule < ::Gitlab::Config::Entry::Node include ::Gitlab::Config::Entry::Validatable + include ::Gitlab::Config::Entry::Configurable include ::Gitlab::Config::Entry::Attributable - ALLOWED_KEYS = %i[if exists when].freeze + ALLOWED_KEYS = %i[if exists when changes].freeze ALLOWED_WHEN = %w[never always].freeze attributes :if, :exists, :when + entry :changes, Entry::Rules::Rule::Changes, + description: 'File change condition rule.' + validations do validates :config, presence: true validates :config, type: { with: Hash } @@ -27,7 +31,9 @@ module Gitlab end def value - Feature.enabled?(:ci_refactor_external_rules) ? config.compact : super + config.merge( + changes: (changes_value if changes_defined?) + ).compact end end end diff --git a/lib/gitlab/ci/config/entry/job.rb b/lib/gitlab/ci/config/entry/job.rb index d31d1b366c3..c40d665f320 100644 --- a/lib/gitlab/ci/config/entry/job.rb +++ b/lib/gitlab/ci/config/entry/job.rb @@ -13,7 +13,7 @@ module Gitlab ALLOWED_WHEN = %w[on_success on_failure always manual delayed].freeze ALLOWED_KEYS = %i[tags script image services start_in artifacts cache dependencies before_script after_script hooks - environment coverage retry parallel interruptible timeout + coverage retry parallel interruptible timeout release id_tokens publish].freeze validations do @@ -102,10 +102,6 @@ module Gitlab metadata: { allowed_needs: %i[job cross_dependency] }, inherit: false - entry :environment, Entry::Environment, - description: 'Environment configuration for this job.', - inherit: false - entry :coverage, Entry::Coverage, description: 'Coverage configuration for this job.', inherit: false @@ -124,7 +120,7 @@ module Gitlab entry :id_tokens, ::Gitlab::Config::Entry::ComposableHash, description: 'Configured JWTs for this job', - inherit: false, + inherit: true, metadata: { composable_class: ::Gitlab::Ci::Config::Entry::IdToken } entry :publish, Entry::Publish, @@ -160,13 +156,11 @@ module Gitlab when: self.when, start_in: self.start_in, dependencies: dependencies, - environment: environment_defined? ? environment_value : nil, - environment_name: environment_defined? ? environment_value[:name] : nil, coverage: coverage_defined? ? coverage_value : nil, retry: retry_defined? ? retry_value : nil, parallel: has_parallel? ? parallel_value : nil, interruptible: interruptible_defined? ? interruptible_value : nil, - timeout: has_timeout? ? ChronicDuration.parse(timeout.to_s) : nil, + timeout: parsed_timeout, artifacts: artifacts_value, release: release_value, after_script: after_script_value, @@ -180,6 +174,12 @@ module Gitlab ).compact end + def parsed_timeout + return unless has_timeout? + + ChronicDuration.parse(timeout.to_s, use_complete_matcher: true) + end + def ignored? allow_failure_defined? ? static_allow_failure : manual_action? end diff --git a/lib/gitlab/ci/config/entry/processable.rb b/lib/gitlab/ci/config/entry/processable.rb index e0f0903174c..88734ac1186 100644 --- a/lib/gitlab/ci/config/entry/processable.rb +++ b/lib/gitlab/ci/config/entry/processable.rb @@ -15,7 +15,7 @@ module Gitlab include ::Gitlab::Config::Entry::Inheritable PROCESSABLE_ALLOWED_KEYS = %i[extends stage only except rules variables - inherit allow_failure when needs resource_group].freeze + inherit allow_failure when needs resource_group environment].freeze MAX_NESTING_LEVEL = 10 included do @@ -68,6 +68,10 @@ module Gitlab inherit: false, default: {} + entry :environment, Entry::Environment, + description: 'Environment configuration for this job.', + inherit: false + attributes :extends, :rules, :resource_group end @@ -125,6 +129,8 @@ module Gitlab root_variables_inheritance: root_variables_inheritance, only: only_value, except: except_value, + environment: environment_defined? ? environment_value : nil, + environment_name: environment_defined? ? environment_value[:name] : nil, resource_group: resource_group }.compact end diff --git a/lib/gitlab/ci/config/external/context.rb b/lib/gitlab/ci/config/external/context.rb index c57391d355c..0a524fdba66 100644 --- a/lib/gitlab/ci/config/external/context.rb +++ b/lib/gitlab/ci/config/external/context.rb @@ -9,22 +9,21 @@ module Gitlab TimeoutError = Class.new(StandardError) - TEMP_MAX_INCLUDES = 100 # For logging; to be removed in https://gitlab.com/gitlab-org/gitlab/-/issues/396776 - include ::Gitlab::Utils::StrongMemoize attr_reader :project, :sha, :user, :parent_pipeline, :variables, :pipeline_config - attr_reader :expandset, :execution_deadline, :logger, :max_includes, :max_total_yaml_size_bytes + attr_reader :pipeline, :expandset, :execution_deadline, :logger, :max_includes, :max_total_yaml_size_bytes attr_accessor :total_file_size_in_bytes delegate :instrument, to: :logger def initialize( - project: nil, sha: nil, user: nil, parent_pipeline: nil, variables: nil, + project: nil, pipeline: nil, sha: nil, user: nil, parent_pipeline: nil, variables: nil, pipeline_config: nil, logger: nil ) @project = project + @pipeline = pipeline @sha = sha @user = user @parent_pipeline = parent_pipeline @@ -60,6 +59,7 @@ module Gitlab def mutate(attrs = {}) self.class.new(**attrs) do |ctx| + ctx.pipeline = pipeline ctx.expandset = expandset ctx.execution_deadline = execution_deadline ctx.logger = logger @@ -106,7 +106,7 @@ module Gitlab protected - attr_writer :expandset, :execution_deadline, :logger, :max_includes, :max_total_yaml_size_bytes + attr_writer :pipeline, :expandset, :execution_deadline, :logger, :max_includes, :max_total_yaml_size_bytes private diff --git a/lib/gitlab/ci/config/external/file/component.rb b/lib/gitlab/ci/config/external/file/component.rb index 15cc0783b86..de6de1bb7a8 100644 --- a/lib/gitlab/ci/config/external/file/component.rb +++ b/lib/gitlab/ci/config/external/file/component.rb @@ -18,6 +18,8 @@ module Gitlab def content return unless component_result.success? + ::Gitlab::UsageDataCounters::HLLRedisCounter.track_event('cicd_component_usage', values: context.user.id) + component_result.payload.fetch(:content) end strong_memoize_attr :content diff --git a/lib/gitlab/ci/config/external/mapper/verifier.rb b/lib/gitlab/ci/config/external/mapper/verifier.rb index 580cae8a207..0e296aa0b5b 100644 --- a/lib/gitlab/ci/config/external/mapper/verifier.rb +++ b/lib/gitlab/ci/config/external/mapper/verifier.rb @@ -40,7 +40,7 @@ module Gitlab file.validate_content! if file.valid? file.load_and_validate_expanded_hash! if file.valid? - next unless Feature.enabled?(:introduce_ci_max_total_yaml_size_bytes, context.project) && file.valid? + next unless file.valid? # We are checking the file.content.to_s because that is returning the actual content of the file, # whereas file.content would return the BatchLoader. diff --git a/lib/gitlab/ci/config/external/rules.rb b/lib/gitlab/ci/config/external/rules.rb index 0e6209460e0..05266fbff0c 100644 --- a/lib/gitlab/ci/config/external/rules.rb +++ b/lib/gitlab/ci/config/external/rules.rb @@ -5,29 +5,19 @@ module Gitlab class Config module External class Rules - # Remove these two constants when FF `ci_refactor_external_rules` is removed - ALLOWED_KEYS = Entry::Include::Rules::Rule::ALLOWED_KEYS - ALLOWED_WHEN = Entry::Include::Rules::Rule::ALLOWED_WHEN - InvalidIncludeRulesError = Class.new(Mapper::Error) def initialize(rule_hashes) - if Feature.enabled?(:ci_refactor_external_rules) - return unless rule_hashes - - # We must compose the include rules entry here because included - # files are expanded before `@root.compose!` runs in Ci::Config. - rules_entry = Entry::Include::Rules.new(rule_hashes) - rules_entry.compose! + return unless rule_hashes - raise InvalidIncludeRulesError, "include:#{rules_entry.errors.first}" unless rules_entry.valid? + # We must compose the include rules entry here because included + # files are expanded before `@root.compose!` runs in Ci::Config. + rules_entry = Entry::Include::Rules.new(rule_hashes) + rules_entry.compose! - @rule_list = Build::Rules::Rule.fabricate_list(rules_entry.value) - else - validate(rule_hashes) + raise InvalidIncludeRulesError, "include:#{rules_entry.errors.first}" unless rules_entry.valid? - @rule_list = Build::Rules::Rule.fabricate_list(rule_hashes) - end + @rule_list = Build::Rules::Rule.fabricate_list(rules_entry.value) end def evaluate(context) @@ -38,28 +28,14 @@ module Gitlab else Result.new('never') end + rescue Build::Rules::Rule::Clause::ParseError => e + raise InvalidIncludeRulesError, "include:#{e.message}" end private def match_rule(context) - @rule_list.find { |rule| rule.matches?(nil, context) } - end - - # Remove this method when FF `ci_refactor_external_rules` is removed - def validate(rule_hashes) - return unless rule_hashes.is_a?(Array) - - rule_hashes.each do |rule_hash| - next if (rule_hash.keys - ALLOWED_KEYS).empty? && valid_when?(rule_hash) - - raise InvalidIncludeRulesError, "invalid include rule: #{rule_hash}" - end - end - - # Remove this method when FF `ci_refactor_external_rules` is removed - def valid_when?(rule_hash) - rule_hash[:when].nil? || rule_hash[:when].in?(ALLOWED_WHEN) + @rule_list.find { |rule| rule.matches?(context.pipeline, context) } end Result = Struct.new(:when) do diff --git a/lib/gitlab/ci/config/interpolation/interpolator.rb b/lib/gitlab/ci/config/interpolation/interpolator.rb index 58965890184..95c419d7427 100644 --- a/lib/gitlab/ci/config/interpolation/interpolator.rb +++ b/lib/gitlab/ci/config/interpolation/interpolator.rb @@ -37,7 +37,12 @@ module Gitlab def interpolate! return @errors.push(config.error) unless config.valid? - return @errors.push('unknown input arguments') if inputs_without_header? + + if inputs_without_header? + return @errors.push( + _('Given inputs not defined in the `spec` section of the included configuration file')) + end + return @result ||= config.content unless config.has_header? return @errors.concat(header.errors) unless header.valid? diff --git a/lib/gitlab/ci/jwt_v2.rb b/lib/gitlab/ci/jwt_v2.rb index 8c730a9548f..29beba4774a 100644 --- a/lib/gitlab/ci/jwt_v2.rb +++ b/lib/gitlab/ci/jwt_v2.rb @@ -61,10 +61,6 @@ module Gitlab pipeline_source: pipeline.source&.to_sym, pipeline_source_bridge: pipeline.source_bridge ) - rescue StandardError => e - # We don't want endpoints relying on this code to fail if there's an error here. - Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e, pipeline_id: pipeline.id) - nil end strong_memoize_attr(:project_config) diff --git a/lib/gitlab/ci/parsers/sbom/cyclonedx.rb b/lib/gitlab/ci/parsers/sbom/cyclonedx.rb index bc62fbe55ec..1e5200e8682 100644 --- a/lib/gitlab/ci/parsers/sbom/cyclonedx.rb +++ b/lib/gitlab/ci/parsers/sbom/cyclonedx.rb @@ -58,6 +58,15 @@ module Gitlab properties = data.dig('metadata', 'properties') source = CyclonedxProperties.parse_source(properties) report.set_source(source) if source + + tools = data.dig('metadata', 'tools') + authors = data.dig('metadata', 'authors') + + report.metadata = ::Gitlab::Ci::Reports::Sbom::Metadata.new.tap do |metadata| + metadata.tools = tools if tools + metadata.authors = authors if authors + metadata.properties = properties if properties + end end def parse_components diff --git a/lib/gitlab/ci/pipeline/chain/command.rb b/lib/gitlab/ci/pipeline/chain/command.rb index 4bc2f6c7be7..cc3aa33e93b 100644 --- a/lib/gitlab/ci/pipeline/chain/command.rb +++ b/lib/gitlab/ci/pipeline/chain/command.rb @@ -128,10 +128,6 @@ module Gitlab .observe({ plan: project.actual_plan_name }, jobs_count) end - def observe_pipeline_includes_count(pipeline) - logger.observe(:pipeline_includes_count, pipeline.config_metadata&.[](:includes)&.count, once: true) - end - def increment_pipeline_failure_reason_counter(reason) metrics.pipeline_failure_reason_counter .increment(reason: (reason || :unknown_failure).to_s) diff --git a/lib/gitlab/ci/pipeline/chain/sequence.rb b/lib/gitlab/ci/pipeline/chain/sequence.rb index dd097187955..de147914850 100644 --- a/lib/gitlab/ci/pipeline/chain/sequence.rb +++ b/lib/gitlab/ci/pipeline/chain/sequence.rb @@ -30,7 +30,6 @@ module Gitlab @command.observe_creation_duration(current_monotonic_time - @start) @command.observe_pipeline_size(@pipeline) @command.observe_jobs_count_in_alive_pipelines - @command.observe_pipeline_includes_count(@pipeline) @pipeline end diff --git a/lib/gitlab/ci/queue/metrics.rb b/lib/gitlab/ci/queue/metrics.rb index a18542288c9..db1b53e52e0 100644 --- a/lib/gitlab/ci/queue/metrics.rb +++ b/lib/gitlab/ci/queue/metrics.rb @@ -14,7 +14,6 @@ module Gitlab METRICS_SHARD_TAG_PREFIX = 'metrics_shard::' DEFAULT_METRICS_SHARD = 'default' - JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET = 5 OPERATION_COUNTERS = [ :build_can_pick, @@ -57,7 +56,7 @@ module Gitlab def register_success(job) labels = { shared_runner: runner.instance_type?, - jobs_running_for_project: jobs_running_for_project(job), + jobs_running_for_project: job.project_jobs_running_on_instance_runners_count, shard: DEFAULT_METRICS_SHARD } if runner.instance_type? @@ -65,7 +64,7 @@ module Gitlab labels[:shard] = shard.gsub(METRICS_SHARD_TAG_PREFIX, '') if shard end - self.class.job_queue_duration_seconds.observe(labels, Time.current - job.queued_at) unless job.queued_at.nil? + self.class.job_queue_duration_seconds.observe(labels, job.time_in_queue_seconds) unless job.queued_at.nil? self.class.attempt_counter.increment end @@ -231,28 +230,6 @@ module Gitlab Gitlab::Metrics.histogram(name, comment, labels, buckets) end end - - private - - # rubocop: disable CodeReuse/ActiveRecord - def jobs_running_for_project(job) - return '+Inf' unless runner.instance_type? - - # excluding currently started job - running_jobs_count = running_jobs_relation(job) - .limit(JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET + 1).count - 1 - - if running_jobs_count < JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET - running_jobs_count - else - "#{JOBS_RUNNING_FOR_PROJECT_MAX_BUCKET}+" - end - end - - def running_jobs_relation(job) - ::Ci::RunningBuild.instance_type.where(project_id: job.project_id) - end - # rubocop: enable CodeReuse/ActiveRecord end end end diff --git a/lib/gitlab/ci/reports/codequality_reports.rb b/lib/gitlab/ci/reports/codequality_reports.rb index aba2d2e8b19..edcb17a61a7 100644 --- a/lib/gitlab/ci/reports/codequality_reports.rb +++ b/lib/gitlab/ci/reports/codequality_reports.rb @@ -6,7 +6,7 @@ module Gitlab class CodequalityReports attr_reader :degradations, :error_message - SEVERITY_PRIORITIES = %w(blocker critical major minor info unknown).map.with_index.to_h.freeze # { "blocker" => 0, "critical" => 1 ... } + SEVERITY_PRIORITIES = %w[blocker critical major minor info unknown].map.with_index.to_h.freeze # { "blocker" => 0, "critical" => 1 ... } CODECLIMATE_SCHEMA_PATH = Rails.root.join('app', 'validators', 'json_schemas', 'codeclimate.json').to_s def initialize diff --git a/lib/gitlab/ci/reports/sbom/component.rb b/lib/gitlab/ci/reports/sbom/component.rb index 51fd6af7bc4..59816e75b2c 100644 --- a/lib/gitlab/ci/reports/sbom/component.rb +++ b/lib/gitlab/ci/reports/sbom/component.rb @@ -7,7 +7,7 @@ module Gitlab class Component include Gitlab::Utils::StrongMemoize - attr_reader :component_type, :version + attr_reader :component_type, :version, :path def initialize(type:, name:, purl:, version:) @component_type = type @@ -31,12 +31,24 @@ module Gitlab end strong_memoize_attr :purl + def purl_type + purl.type + end + + def type + component_type + end + def name return @name unless purl [purl.namespace, purl.name].compact.join('/') end + def key + [name, version, purl&.type] + end + private def supported_component_type? diff --git a/lib/gitlab/ci/reports/sbom/metadata.rb b/lib/gitlab/ci/reports/sbom/metadata.rb new file mode 100644 index 00000000000..8945259d331 --- /dev/null +++ b/lib/gitlab/ci/reports/sbom/metadata.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Reports + module Sbom + class Metadata + attr_accessor :tools, :authors, :properties, :timestamp + + def initialize(tools: [], authors: [], properties: []) + @tools = tools + @authors = authors + @properties = properties + end + end + end + end + end +end diff --git a/lib/gitlab/ci/reports/sbom/report.rb b/lib/gitlab/ci/reports/sbom/report.rb index 51fa8ce0d2e..9a71c67388d 100644 --- a/lib/gitlab/ci/reports/sbom/report.rb +++ b/lib/gitlab/ci/reports/sbom/report.rb @@ -5,10 +5,24 @@ module Gitlab module Reports module Sbom class Report - attr_reader :components, :source, :errors + # This represents the attributes defined in cycloneDX Schema + # https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/validators/json_schemas/cyclonedx_report.json#L7 + BOM_FORMAT = 'CycloneDX' + SPEC_VERSION = '1.4' + VERSION = 1 + + attr_reader :source, :errors + attr_accessor :sbom_attributes, :metadata, :components def initialize + @sbom_attributes = { + bom_format: BOM_FORMAT, + spec_version: SPEC_VERSION, + serial_number: "urn:uuid:#{SecureRandom.uuid}", + version: VERSION + } @components = [] + @metadata = ::Gitlab::Ci::Reports::Sbom::Metadata.new @errors = [] end diff --git a/lib/gitlab/ci/reports/test_reports_comparer.rb b/lib/gitlab/ci/reports/test_reports_comparer.rb index 497831ae5a7..0ea2d793eea 100644 --- a/lib/gitlab/ci/reports/test_reports_comparer.rb +++ b/lib/gitlab/ci/reports/test_reports_comparer.rb @@ -29,7 +29,7 @@ module Gitlab end end - %w(total_count resolved_count failed_count error_count).each do |method| + %w[total_count resolved_count failed_count error_count].each do |method| define_method(method) do # rubocop: disable CodeReuse/ActiveRecord suite_comparers.sum { |suite| suite.public_send(method) } # rubocop:disable GitlabSecurity/PublicSend diff --git a/lib/gitlab/ci/reports/test_suite.rb b/lib/gitlab/ci/reports/test_suite.rb index dcc593b4403..c681727a43d 100644 --- a/lib/gitlab/ci/reports/test_suite.rb +++ b/lib/gitlab/ci/reports/test_suite.rb @@ -77,7 +77,7 @@ module Gitlab def +(other) self.class.new.tap do |test_suite| - test_suite.name = self.name + test_suite.name = other.name test_suite.test_cases = self.test_cases.deep_merge(other.test_cases) test_suite.total_time = self.total_time + other.total_time end diff --git a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml index 6e2faf33a2f..fa1d8bec7e6 100644 --- a/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Auto-DevOps.gitlab-ci.yml @@ -65,6 +65,10 @@ variables: DOCKER_TLS_CERTDIR: "" # https://gitlab.com/gitlab-org/gitlab-runner/issues/4501 + # License-Scanning job is removed from GitLab 16.3 + # This is the fix for https://gitlab.com/gitlab-org/gitlab/-/issues/422791 + LICENSE_MANAGEMENT_DISABLED: "true" + stages: - build - test diff --git a/lib/gitlab/ci/templates/Cosign.gitlab-ci.yml b/lib/gitlab/ci/templates/Cosign.gitlab-ci.yml new file mode 100644 index 00000000000..48c9422b469 --- /dev/null +++ b/lib/gitlab/ci/templates/Cosign.gitlab-ci.yml @@ -0,0 +1,22 @@ +# To contribute improvements to CI/CD templates, please follow the Development guide at: +# https://docs.gitlab.com/ee/development/cicd/templates.html +# This specific template is located at: +# https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Cosign.gitlab-ci.yml + +# This template extends Docker.gitlab-ci.yml to sign the image with Cosign after building. +# This allows you to verify that an image was built by a trusted pipeline before running it. +# See https://docs.gitlab.com/ee/ci/yaml/signing_examples.html for more details. + +include: + template: Docker.gitlab-ci.yml + +docker-build: + variables: + COSIGN_YES: "true" # Used by Cosign to skip confirmation prompts for non-destructive operations + id_tokens: + SIGSTORE_ID_TOKEN: # Used by Cosign to get certificate from Fulcio + aud: sigstore + after_script: + - apk add --update cosign + - IMAGE_DIGEST="$(docker inspect --format='{{index .RepoDigests 0}}' "$DOCKER_IMAGE_NAME")" + - cosign sign "$IMAGE_DIGEST" diff --git a/lib/gitlab/ci/templates/Docker.gitlab-ci.yml b/lib/gitlab/ci/templates/Docker.gitlab-ci.yml index 8f5f0e2c451..1aa346aec67 100644 --- a/lib/gitlab/ci/templates/Docker.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Docker.gitlab-ci.yml @@ -15,21 +15,20 @@ docker-build: stage: build services: - docker:dind + variables: + DOCKER_IMAGE_NAME: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG before_script: - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY - # Default branch leaves tag empty (= latest tag) - # All other branches are tagged with the escaped branch name (commit ref slug) + # All branches are tagged with $DOCKER_IMAGE_NAME (defaults to commit ref slug) + # Default branch is also tagged with `latest` script: + - docker build --pull -t "$DOCKER_IMAGE_NAME" . + - docker push "$DOCKER_IMAGE_NAME" - | if [[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]]; then - tag="" - echo "Running on default branch '$CI_DEFAULT_BRANCH': tag = 'latest'" - else - tag=":$CI_COMMIT_REF_SLUG" - echo "Running on branch '$CI_COMMIT_BRANCH': tag = $tag" + docker tag "$DOCKER_IMAGE_NAME" "$CI_REGISTRY_IMAGE:latest" + docker push "$CI_REGISTRY_IMAGE:latest" fi - - docker build --pull -t "$CI_REGISTRY_IMAGE${tag}" . - - docker push "$CI_REGISTRY_IMAGE${tag}" # Run this job in a branch where a Dockerfile exists rules: - if: $CI_COMMIT_BRANCH diff --git a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml index c1aedbe1111..07bc3fbe795 100644 --- a/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Build.gitlab-ci.yml @@ -1,5 +1,5 @@ variables: - AUTO_BUILD_IMAGE_VERSION: 'v1.38.1' + AUTO_BUILD_IMAGE_VERSION: 'v1.41.0' build: stage: build diff --git a/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml index c1aedbe1111..07bc3fbe795 100644 --- a/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Build.latest.gitlab-ci.yml @@ -1,5 +1,5 @@ variables: - AUTO_BUILD_IMAGE_VERSION: 'v1.38.1' + AUTO_BUILD_IMAGE_VERSION: 'v1.41.0' build: stage: build diff --git a/lib/gitlab/ci/templates/Jobs/Container-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Container-Scanning.gitlab-ci.yml index 192d06bfa14..5cee19a746c 100644 --- a/lib/gitlab/ci/templates/Jobs/Container-Scanning.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Container-Scanning.gitlab-ci.yml @@ -40,6 +40,7 @@ container_scanning: reports: container_scanning: gl-container-scanning-report.json dependency_scanning: gl-dependency-scanning-report.json + cyclonedx: "**/gl-sbom-*.cdx.json" paths: [gl-container-scanning-report.json, gl-dependency-scanning-report.json, "**/gl-sbom-*.cdx.json"] dependencies: [] script: diff --git a/lib/gitlab/ci/templates/Jobs/Container-Scanning.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Container-Scanning.latest.gitlab-ci.yml index 9a4c75e7402..ade4be99f18 100644 --- a/lib/gitlab/ci/templates/Jobs/Container-Scanning.latest.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Container-Scanning.latest.gitlab-ci.yml @@ -40,6 +40,7 @@ container_scanning: reports: container_scanning: gl-container-scanning-report.json dependency_scanning: gl-dependency-scanning-report.json + cyclonedx: "**/gl-sbom-*.cdx.json" paths: [gl-container-scanning-report.json, gl-dependency-scanning-report.json, "**/gl-sbom-*.cdx.json"] dependencies: [] script: diff --git a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml index 7b2fb49b65e..e9ba938142d 100644 --- a/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/DAST-Default-Branch-Deploy.gitlab-ci.yml @@ -1,5 +1,5 @@ variables: - DAST_AUTO_DEPLOY_IMAGE_VERSION: 'v2.53.0' + DAST_AUTO_DEPLOY_IMAGE_VERSION: 'v2.56.0' .dast-auto-deploy: image: "${CI_TEMPLATE_REGISTRY_HOST}/gitlab-org/cluster-integration/auto-deploy-image:${DAST_AUTO_DEPLOY_IMAGE_VERSION}" diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml index 1e482ccca82..eaaf171e4b5 100644 --- a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml @@ -1,5 +1,5 @@ variables: - AUTO_DEPLOY_IMAGE_VERSION: 'v2.53.0' + AUTO_DEPLOY_IMAGE_VERSION: 'v2.56.0' .auto-deploy: image: "${CI_TEMPLATE_REGISTRY_HOST}/gitlab-org/cluster-integration/auto-deploy-image:${AUTO_DEPLOY_IMAGE_VERSION}" diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml index 6eac691b293..d2e448fb6a1 100644 --- a/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Jobs/Deploy.latest.gitlab-ci.yml @@ -1,5 +1,5 @@ variables: - AUTO_DEPLOY_IMAGE_VERSION: 'v2.53.0' + AUTO_DEPLOY_IMAGE_VERSION: 'v2.56.0' .auto-deploy: image: "${CI_TEMPLATE_REGISTRY_HOST}/gitlab-org/cluster-integration/auto-deploy-image:${AUTO_DEPLOY_IMAGE_VERSION}" diff --git a/lib/gitlab/ci/templates/MATLAB.gitlab-ci.yml b/lib/gitlab/ci/templates/MATLAB.gitlab-ci.yml index 30767e66649..1468cf9c7c6 100644 --- a/lib/gitlab/ci/templates/MATLAB.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/MATLAB.gitlab-ci.yml @@ -3,17 +3,17 @@ # This specific template is located at: # https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/MATLAB.gitlab-ci.yml -# Use this template to run MATLAB and Simulink as part of your CI/CD pipeline. The template includes three jobs: +# Use this template to build and test your MATLAB project as part of your CI/CD pipeline. The template includes four jobs: # - `command`: Run MATLAB scripts, functions, and statements. # - `test`: Run tests authored using the MATLAB unit testing framework or Simulink Test. # - `test_artifacts`: Run MATLAB and Simulink tests, and generate test and coverage artifacts. +# - `build`: Run a build using the MATLAB build tool. # # The jobs in the template use the `matlab -batch` syntax to start MATLAB. The `-batch` option is supported # in MATLAB R2019a and later. # # You can copy and paste one or more jobs in this template into your `.gitlab-ci.yml` file. # You should not add this template to an existing `.gitlab-ci.yml` file by using the `include:` keyword. -# # Your runner must use the Docker executor to run MATLAB within a container. The [MATLAB Container on Docker Hub][1] # lets you run your build using MATLAB R2020b or a later release. If your build requires additional toolboxes, use a @@ -24,7 +24,7 @@ # [2] https://www.mathworks.com/help/cloudcenter/ug/create-a-custom-matlab-container.html # The jobs in this template incorporate the contents of a hidden `.matlab_defaults` job. You need to -# configure this job before running the `command`, `test`, and `test_artifacts` jobs. To configure the job: +# configure this job before running the `command`, `test`, `test_artifacts`, and `build` jobs. To configure the job: # - Specify the name of the MATLAB container image you want to use. # - Set the `MLM_LICENSE_FILE` environment variable using the port number and DNS address for your network license manager. # @@ -40,17 +40,17 @@ # command: extends: .matlab_defaults - script: matlab -batch mycommand + script: matlab -batch "mycommand" # If you specify more than one script, function, or statement, use a comma or semicolon to separate them. # For example, to run `myscript.m` in a folder named `myfolder` located in the root of the repository, -# you can specify `mycommand` like this: +# you can specify `"mycommand"` like this: # # "addpath('myfolder'), myscript" # # MATLAB exits with exit code 0 if the specified script, function, or statement executes successfully without # error. Otherwise, MATLAB terminates with a nonzero exit code, which causes the job to fail. To have the -# job fail in certain conditions, use the [`assert`][3] or [`error`][4] functions. +# job fail in certain conditions, use the [`assert`][3] or [`error`][4] function. # # [3] https://www.mathworks.com/help/matlab/ref/assert.html # [4] https://www.mathworks.com/help/matlab/ref/error.html @@ -62,7 +62,7 @@ test: extends: .matlab_defaults script: matlab -batch "results = runtests('IncludeSubfolders',true), assertSuccess(results);" -# By default, the job includes any files in your [MATLAB Project][7] that have a `Test` label. If your repository +# By default, the job includes any files in your [MATLAB project][7] that have a `Test` label. If your repository # does not have a MATLAB project, then the job includes all tests in the root of your repository or in any of # its subfolders. # @@ -71,9 +71,9 @@ test: # [7] https://www.mathworks.com/help/matlab/projects.html # The `test_artifacts` job runs your tests and additionally generates test and coverage artifacts. -# It uses the plugin classes in the [`matlab.unittest.plugins`][8] package to generate a JUnit test results -# report and a Cobertura code coverage report. Like the `test` job, this job runs all the tests in your -# project and fails the build if any of the tests fail. +# It uses the plugin classes in the [`matlab.unittest.plugins`][8] package to produce test results +# in JUnit-style XML format and code coverage results in Cobertura XML format. Like the `test` job, +# this job runs all the tests in your project and fails the build if any of the tests fail. # test_artifacts: extends: .matlab_defaults @@ -110,3 +110,22 @@ test_artifacts: # # [8] https://www.mathworks.com/help/matlab/ref/matlab.unittest.plugins-package.html # [9] https://www.mathworks.com/help/matlab/matlab_prog/generate-artifacts-using-matlab-unit-test-plugins.html + +# Starting in R2022b, the `build` job runs a build using the MATLAB build tool. You can use this job to run the +# tasks specified in a file named `buildfile.m` in the root of your repository. +# +build: + extends: .matlab_defaults + script: matlab -batch "buildtool" + +# The job executes the [`buildtool`][10] command to run a build using the default tasks in `buildfile.m` +# as well as all the tasks on which they depend. To run specific tasks instead, specify them as a space-separated +# list in the job. For example, to run the tasks named `task1` and `task2` and their dependencies, substitute +# `"buildtool"` with `"buildtool task1 task2"`. +# +# MATLAB exits with exit code 0 if the build runs successfully. Otherwise, MATLAB terminates with a nonzero +# exit code, which causes the job to fail. For more information about the MATLAB build tool, +# see [Create and Run Tasks Using Build Tool][11]. +# +# [10] https://www.mathworks.com/help/matlab/ref/buildtool.html +# [11] https://www.mathworks.com/help/matlab/matlab_prog/create-and-run-tasks-using-build-tool.html diff --git a/lib/gitlab/ci/trace.rb b/lib/gitlab/ci/trace.rb index 2dc7bbc391e..f4ba9100812 100644 --- a/lib/gitlab/ci/trace.rb +++ b/lib/gitlab/ci/trace.rb @@ -30,9 +30,9 @@ module Gitlab @job = job end - def html(last_lines: nil) + def html(last_lines: nil, max_size: nil) read do |stream| - stream.html(last_lines: last_lines) + stream.html(last_lines: last_lines, max_size: max_size) end end @@ -290,7 +290,7 @@ module Gitlab if consistent_archived_trace?(build) ::Ci::Build .sticking - .unstick_or_continue_sticking(LOAD_BALANCING_STICKING_NAMESPACE, build.id) + .find_caught_up_replica(LOAD_BALANCING_STICKING_NAMESPACE, build.id) end yield diff --git a/lib/gitlab/ci/trace/stream.rb b/lib/gitlab/ci/trace/stream.rb index dd435ba05b7..ef494a79d9a 100644 --- a/lib/gitlab/ci/trace/stream.rb +++ b/lib/gitlab/ci/trace/stream.rb @@ -53,18 +53,20 @@ module Gitlab append(data, 0) end - def raw(last_lines: nil) + def raw(last_lines: nil, max_size: nil) return unless valid? - if last_lines.to_i > 0 + if max_size.to_i > 0 + read_last_lines_with_max_size(last_lines, max_size) + elsif last_lines.to_i > 0 read_last_lines(last_lines) else stream.read end.force_encoding(Encoding.default_external) end - def html(last_lines: nil) - text = raw(last_lines: last_lines) + def html(last_lines: nil, max_size: nil) + text = raw(last_lines: last_lines, max_size: max_size) buffer = StringIO.new(text) ::Gitlab::Ci::Ansi2html.convert(buffer).html end @@ -117,6 +119,37 @@ module Gitlab to_enum(:reverse_line).first(limit).reverse.join end + def read_last_lines_with_max_size(limit, max_size) + linesleft = limit + result = '' + + reverse_line_with_max_size(max_size) do |line| + result = line + result + unless linesleft.nil? + linesleft -= 1 + break if linesleft <= 0 + end + end + + result + end + + def reverse_line_with_max_size(max_size) + stream.seek(0, IO::SEEK_END) + debris = '' + sizeleft = max_size + + until sizeleft <= 0 || (buf = read_backward([BUFFER_SIZE, sizeleft].min)).empty? + sizeleft -= buf.bytesize + debris, *lines = (buf + debris).each_line.to_a + lines.reverse_each do |line| + yield(line.force_encoding(Encoding.default_external)) + end + end + + yield(debris.force_encoding(Encoding.default_external)) unless debris.empty? + end + def reverse_line stream.seek(0, IO::SEEK_END) debris = '' diff --git a/lib/gitlab/ci/variables/builder.rb b/lib/gitlab/ci/variables/builder.rb index cae3a966bc6..c279af6acfc 100644 --- a/lib/gitlab/ci/variables/builder.rb +++ b/lib/gitlab/ci/variables/builder.rb @@ -17,7 +17,7 @@ module Gitlab def scoped_variables(job, environment:, dependencies:) Gitlab::Ci::Variables::Collection.new.tap do |variables| - variables.concat(predefined_variables(job)) + variables.concat(predefined_variables(job, environment)) variables.concat(project.predefined_variables) variables.concat(pipeline_variables_builder.predefined_variables) variables.concat(job.runner.predefined_variables) if job.runnable? && job.runner @@ -126,7 +126,7 @@ module Gitlab delegate :project, to: :pipeline - def predefined_variables(job) + def predefined_variables(job, environment) Gitlab::Ci::Variables::Collection.new.tap do |variables| variables.append(key: 'CI_JOB_NAME', value: job.name) variables.append(key: 'CI_JOB_NAME_SLUG', value: job_name_slug(job)) @@ -137,8 +137,12 @@ module Gitlab variables.append(key: 'CI_NODE_INDEX', value: job.options[:instance].to_s) if job.options&.include?(:instance) variables.append(key: 'CI_NODE_TOTAL', value: ci_node_total_value(job).to_s) - # Set environment name here so we can access it when evaluating the job's rules - variables.append(key: 'CI_ENVIRONMENT_NAME', value: job.environment) if job.environment + if environment.present? + variables.append(key: 'CI_ENVIRONMENT_NAME', value: environment) + variables.append(key: 'CI_ENVIRONMENT_ACTION', value: job.environment_action) + variables.append(key: 'CI_ENVIRONMENT_TIER', value: job.environment_tier) + variables.append(key: 'CI_ENVIRONMENT_URL', value: job.environment_url) if job.environment_url + end end end diff --git a/lib/gitlab/ci/yaml_processor.rb b/lib/gitlab/ci/yaml_processor.rb index c69d9218a66..3a0173d1548 100644 --- a/lib/gitlab/ci/yaml_processor.rb +++ b/lib/gitlab/ci/yaml_processor.rb @@ -129,6 +129,12 @@ module Gitlab error!("#{name} job: undefined #{dependency_type}: #{dependency}") end + # A parallel job's name is expanded in Config::Normalizer so we must revalidate the name length here + if dependency_type == 'need' && dependency.length > ::Ci::BuildNeed::MAX_JOB_NAME_LENGTH + error!("#{name} job: need `#{dependency}` name is too long " \ + "(maximum is #{::Ci::BuildNeed::MAX_JOB_NAME_LENGTH} characters)") + end + job_stage_index = stage_index(name) dependency_stage_index = stage_index(dependency) |