Class: Ci::Pipeline

Constant Summary collapse

MAX_OPEN_MERGE_REQUESTS_REFS =
4
PROJECT_ROUTE_AND_NAMESPACE_ROUTE =
{
  project: [:project_feature, :route, { namespace: :route }]
}.freeze
DEFAULT_CONFIG_PATH =
'.gitlab-ci.yml'
CANCELABLE_STATUSES =
(Ci::HasStatus::CANCELABLE_STATUSES + ['manual']).freeze
UNLOCKABLE_STATUSES =
(Ci::Pipeline.completed_statuses + [:manual]).freeze
COUNT_FAILED_JOBS_LIMIT =

UI only shows 100+. TODO: pass constant to UI for SSoT

101

Constants included from AtomicInternalId

AtomicInternalId::MissingValueError

Constants included from Gitlab::OptimisticLocking

Gitlab::OptimisticLocking::MAX_RETRIES

Constants included from HasStatus

HasStatus::ACTIVE_STATUSES, HasStatus::ALIVE_STATUSES, HasStatus::AVAILABLE_STATUSES, HasStatus::BLOCKED_STATUS, HasStatus::COMPLETED_STATUSES, HasStatus::COMPLETED_WITH_MANUAL_STATUSES, HasStatus::DEFAULT_STATUS, HasStatus::EXECUTING_STATUSES, HasStatus::IGNORED_STATUSES, HasStatus::ORDERED_STATUSES, HasStatus::PASSED_WITH_WARNINGS_STATUSES, HasStatus::STARTED_STATUSES, HasStatus::STATUSES_ENUM, HasStatus::STOPPED_STATUSES, HasStatus::UnknownStatusError

Constants inherited from ApplicationRecord

ApplicationRecord::MAX_PLUCK

Constants included from HasCheckConstraints

HasCheckConstraints::NOT_NULL_CHECK_PATTERN

Constants included from ResetOnColumnErrors

ResetOnColumnErrors::MAX_RESET_PERIOD

Instance Attribute Summary collapse

Attributes included from Importable

#importing, #user_contributions

Class Method Summary collapse

Instance Method Summary collapse

Methods included from FastDestroyAll::Helpers

#perform_fast_destroy

Methods included from AfterCommitQueue

#run_after_commit, #run_after_commit_or_now

Methods included from HasRef

#branch?, #ref_slug

Methods included from AtomicInternalId

group_init, #internal_id_read_scope, #internal_id_scope_attrs, #internal_id_scope_usage, namespace_init, project_init, scope_attrs, scope_usage

Methods included from Gitlab::OptimisticLocking

log_optimistic_lock_retries, retry_lock, retry_lock_histogram, retry_lock_logger

Methods included from Gitlab::Allowable

#can?, #can_all?, #can_any?

Methods included from Presentable

#present

Methods included from HasStatus

#active?, #blocked?, #complete?, #complete_or_manual?, #incomplete?, #started?

Methods included from Partitionable

registered_models

Methods inherited from ApplicationRecord

model_name, table_name_prefix

Methods inherited from ApplicationRecord

===, cached_column_list, #create_or_load_association, declarative_enum, default_select_columns, id_in, id_not_in, iid_in, nullable_column?, pluck_primary_key, primary_key_in, #readable_by?, safe_ensure_unique, safe_find_or_create_by, safe_find_or_create_by!, #to_ability_name, underscore, where_exists, where_not_exists, with_fast_read_statement_timeout, without_order

Methods included from ResetOnColumnErrors

#reset_on_union_error, #reset_on_unknown_attribute_error

Methods included from Gitlab::SensitiveSerializableHash

#serializable_hash

Instance Attribute Details

#config_metadataObject

Ci::CreatePipelineService returns Ci::Pipeline so this is the only place where we can pass additional information from the service. This accessor is used for storing the processed metadata for linting purposes. There is an open issue to address this: gitlab.com/gitlab-org/gitlab/-/issues/259010



51
52
53
# File 'app/models/ci/pipeline.rb', line 51

def 
  @config_metadata
end

Class Method Details

.auto_devops_pipelines_completed_totalObject



588
589
590
# File 'app/models/ci/pipeline.rb', line 588

def self.auto_devops_pipelines_completed_total
  @auto_devops_pipelines_completed_total ||= Gitlab::Metrics.counter(:auto_devops_pipelines_completed_total, 'Number of completed auto devops pipelines')
end

.bridgeable_statusesObject



584
585
586
# File 'app/models/ci/pipeline.rb', line 584

def self.bridgeable_statuses
  ::Ci::Pipeline::AVAILABLE_STATUSES - %w[created waiting_for_resource waiting_for_callback preparing pending]
end

.builds_count_in_alive_pipelinesObject



541
542
543
# File 'app/models/ci/pipeline.rb', line 541

def self.builds_count_in_alive_pipelines
  created_after(24.hours.ago).alive.joins(:builds).count
end

.current_partition_valueObject



592
593
594
595
596
# File 'app/models/ci/pipeline.rb', line 592

def self.current_partition_value
  Gitlab::SafeRequestStore.fetch(:ci_current_partition_value) do
    Ci::Partition.current&.id || Ci::Partition::INITIAL_PARTITION_VALUE
  end
end

.internal_id_scope_usageObject



602
603
604
# File 'app/models/ci/pipeline.rb', line 602

def self.internal_id_scope_usage
  :ci_pipelines
end

.internal_sourcesObject



580
581
582
# File 'app/models/ci/pipeline.rb', line 580

def self.internal_sources
  sources.reject { |source| source == "external" }.values
end

.jobs_count_in_alive_pipelinesObject



537
538
539
# File 'app/models/ci/pipeline.rb', line 537

def self.jobs_count_in_alive_pipelines
  created_after(24.hours.ago).alive.joins(:statuses).count
end

.last_finished_for_ref_id(ci_ref_id) ⇒ Object



568
569
570
# File 'app/models/ci/pipeline.rb', line 568

def self.last_finished_for_ref_id(ci_ref_id)
  where(ci_ref_id: ci_ref_id).ci_sources.finished.order(id: :desc).select(:id).take
end

.latest_failed_for_ref(ref) ⇒ Object



533
534
535
# File 'app/models/ci/pipeline.rb', line 533

def self.latest_failed_for_ref(ref)
  newest_first(ref: ref).failed.take
end

.latest_pipeline_per_commit(commits, ref = nil) ⇒ Object

Returns a Hash containing the latest pipeline for every given commit.

The keys of this Hash are the commit SHAs, the values the pipelines.

commits - The list of commit SHAs to get the pipelines for. ref - The ref to scope the data to (e.g. “master”). If the ref is not

given we simply get the latest pipelines for the commits, regardless
of what refs the pipelines belong to.


554
555
556
557
558
559
560
561
562
# File 'app/models/ci/pipeline.rb', line 554

def self.latest_pipeline_per_commit(commits, ref = nil)
  sql = select('DISTINCT ON (sha) *')
          .where(sha: commits)
          .order(:sha, id: :desc)

  sql = sql.where(ref: ref) if ref

  sql.index_by(&:sha)
end

.latest_running_for_ref(ref) ⇒ Object



529
530
531
# File 'app/models/ci/pipeline.rb', line 529

def self.latest_running_for_ref(ref)
  newest_first(ref: ref).running.take
end

.latest_status(ref = nil) ⇒ Object



504
505
506
# File 'app/models/ci/pipeline.rb', line 504

def self.latest_status(ref = nil)
  newest_first(ref: ref).pick(:status)
end

.latest_successful_for_ref(ref) ⇒ Object



508
509
510
# File 'app/models/ci/pipeline.rb', line 508

def self.latest_successful_for_ref(ref)
  newest_first(ref: ref).success.take
end

.latest_successful_for_refs(refs) ⇒ Object



516
517
518
519
520
521
522
523
524
525
526
527
# File 'app/models/ci/pipeline.rb', line 516

def self.latest_successful_for_refs(refs)
  return Ci::Pipeline.none if refs.empty?

  refs_values = refs.map { |ref| "(#{connection.quote(ref)})" }.join(",")
  query = Arel.sql(sanitize_sql_array(["refs_values.ref = #{quoted_table_name}.ref"]))
  join_query = success.where(query).order(id: :desc).limit(1)

  Ci::Pipeline
    .from("(VALUES #{refs_values}) refs_values (ref)")
    .joins("INNER JOIN LATERAL (#{join_query.to_sql}) #{quoted_table_name} ON TRUE")
    .index_by(&:ref)
end

.latest_successful_for_sha(sha) ⇒ Object



512
513
514
# File 'app/models/ci/pipeline.rb', line 512

def self.latest_successful_for_sha(sha)
  newest_first(sha: sha).success.take
end

.latest_successful_ids_per_projectObject



564
565
566
# File 'app/models/ci/pipeline.rb', line 564

def self.latest_successful_ids_per_project
  success.group(:project_id).select('max(id) as id')
end

.newest_first(ref: nil, sha: nil, limit: nil) ⇒ Object

Returns the pipelines in descending order (= newest first), optionally limited to a number of references.

ref - The name (or names) of the branch(es)/tag(s) to limit the list of

pipelines to.

sha - The commit SHA (or multiple SHAs) to limit the list of pipelines to. limit - Number of pipelines to return. Chaining with sampling methods (#pick, #take)

will cause unnecessary subqueries.


491
492
493
494
495
496
497
498
499
500
501
502
# File 'app/models/ci/pipeline.rb', line 491

def self.newest_first(ref: nil, sha: nil, limit: nil)
  relation = order(id: :desc)
  relation = relation.where(ref: ref) if ref
  relation = relation.where(sha: sha) if sha

  if limit
    ids = relation.limit(limit).select(:id)
    relation = relation.where(id: ids)
  end

  relation
end

.object_hierarchy(relation, options = {}) ⇒ Object



598
599
600
# File 'app/models/ci/pipeline.rb', line 598

def self.object_hierarchy(relation, options = {})
  ::Gitlab::Ci::PipelineObjectHierarchy.new(relation, options: options)
end

.total_durationObject



576
577
578
# File 'app/models/ci/pipeline.rb', line 576

def self.total_duration
  where.not(duration: nil).sum(:duration)
end

.truncate_sha(sha) ⇒ Object



572
573
574
# File 'app/models/ci/pipeline.rb', line 572

def self.truncate_sha(sha)
  sha[0...8]
end

Instance Method Details

#accessibility_reportsObject



1187
1188
1189
1190
1191
1192
1193
# File 'app/models/ci/pipeline.rb', line 1187

def accessibility_reports
  Gitlab::Ci::Reports::AccessibilityReports.new.tap do |accessibility_reports|
    latest_report_builds(Ci::JobArtifact.of_report_type(:accessibility)).each do |build|
      build.collect_accessibility_reports!(accessibility_reports)
    end
  end
end

#add_error_message(content) ⇒ Object



833
834
835
# File 'app/models/ci/pipeline.rb', line 833

def add_error_message(content)
  add_message(:error, content)
end

#add_warning_message(content) ⇒ Object



837
838
839
# File 'app/models/ci/pipeline.rb', line 837

def add_warning_message(content)
  add_message(:warning, content)
end

#age_in_minutesObject

Raises:

  • (ArgumentError)


1395
1396
1397
1398
1399
1400
1401
1402
1403
# File 'app/models/ci/pipeline.rb', line 1395

def age_in_minutes
  return 0 unless persisted?

  raise ArgumentError, 'pipeline not fully loaded' unless has_attribute?(:created_at)

  return 0 unless created_at

  (Time.current - created_at).ceil / 60
end

#all_child_pipelinesObject

With only parent-child pipelines



1051
1052
1053
# File 'app/models/ci/pipeline.rb', line 1051

def all_child_pipelines
  object_hierarchy(project_condition: :same).descendants
end

#all_merge_requestsObject

All the merge requests for which the current pipeline runs/ran against



943
944
945
946
947
948
949
950
951
# File 'app/models/ci/pipeline.rb', line 943

def all_merge_requests
  @all_merge_requests ||=
    if merge_request?
      MergeRequest.where(id: merge_request_id)
    else
      MergeRequest.where(source_project_id: project_id, source_branch: ref)
        .by_commit_sha(sha)
    end
end

#all_merge_requests_by_recencyObject



953
954
955
# File 'app/models/ci/pipeline.rb', line 953

def all_merge_requests_by_recency
  all_merge_requests.order(id: :desc)
end

#all_worktree_pathsObject



1252
1253
1254
1255
1256
# File 'app/models/ci/pipeline.rb', line 1252

def all_worktree_paths
  strong_memoize(:all_worktree_paths) do
    project.repository.ls_files(sha)
  end
end

#auto_cancel_on_job_failureObject



1411
1412
1413
# File 'app/models/ci/pipeline.rb', line 1411

def auto_cancel_on_job_failure
  &.auto_cancel_on_job_failure || 'none'
end

#auto_cancel_on_new_commitObject



1415
1416
1417
# File 'app/models/ci/pipeline.rb', line 1415

def auto_cancel_on_new_commit
  &.auto_cancel_on_new_commit || 'conservative'
end

#auto_canceled?Boolean

Returns:

  • (Boolean)


721
722
723
# File 'app/models/ci/pipeline.rb', line 721

def auto_canceled?
  canceled? && auto_canceled_by_id?
end

#batch_lookup_report_artifact_for_file_type(file_type) ⇒ Object



762
763
764
# File 'app/models/ci/pipeline.rb', line 762

def batch_lookup_report_artifact_for_file_type(file_type)
  batch_lookup_report_artifact_for_file_types([file_type])
end

#batch_lookup_report_artifact_for_file_types(file_types) ⇒ Object



766
767
768
769
770
771
772
773
774
775
# File 'app/models/ci/pipeline.rb', line 766

def batch_lookup_report_artifact_for_file_types(file_types)
  file_types_to_search = []
  file_types.each { |file_type| file_types_to_search.append(*::Ci::JobArtifact.associated_file_types_for(file_type.to_s)) }

  latest_report_artifacts
    .values_at(*file_types_to_search.uniq)
    .flatten
    .compact
    .last
end

#before_shaObject



693
694
695
# File 'app/models/ci/pipeline.rb', line 693

def before_sha
  super || project.repository.blank_ref
end

#branch_updated?Boolean

Returns:

  • (Boolean)


1223
1224
1225
1226
1227
# File 'app/models/ci/pipeline.rb', line 1223

def branch_updated?
  strong_memoize(:branch_updated) do
    push_details.branch_updated?
  end
end

#bridge_triggered?Boolean

Returns:

  • (Boolean)


1079
1080
1081
# File 'app/models/ci/pipeline.rb', line 1079

def bridge_triggered?
  source_bridge.present?
end

#bridge_waiting?Boolean

Returns:

  • (Boolean)


1083
1084
1085
# File 'app/models/ci/pipeline.rb', line 1083

def bridge_waiting?
  source_bridge&.dependent?
end

#bridges_in_self_and_project_descendantsObject



1004
1005
1006
# File 'app/models/ci/pipeline.rb', line 1004

def bridges_in_self_and_project_descendants
  Ci::Bridge.in_partition(self).latest.where(pipeline: self_and_project_descendants)
end

#build_matchersObject



1379
1380
1381
# File 'app/models/ci/pipeline.rb', line 1379

def build_matchers
  self.builds.latest.build_matchers(project)
end

#build_with_artifacts_in_self_and_project_descendants(name) ⇒ Object



993
994
995
996
997
998
# File 'app/models/ci/pipeline.rb', line 993

def build_with_artifacts_in_self_and_project_descendants(name)
  builds_in_self_and_project_descendants
    .ordered_by_pipeline # find job in hierarchical order
    .with_downloadable_artifacts
    .find_by_name(name)
end

#builds_in_self_and_project_descendantsObject



1000
1001
1002
# File 'app/models/ci/pipeline.rb', line 1000

def builds_in_self_and_project_descendants
  Ci::Build.in_partition(self).latest.where(pipeline: self_and_project_descendants)
end

#builds_with_coverageObject



1133
1134
1135
# File 'app/models/ci/pipeline.rb', line 1133

def builds_with_coverage
  builds.latest.with_coverage
end

#builds_with_failed_tests(limit: nil) ⇒ Object



1137
1138
1139
# File 'app/models/ci/pipeline.rb', line 1137

def builds_with_failed_tests(limit: nil)
  latest_test_report_builds.failed.limit(limit)
end

#can_generate_codequality_reports?Boolean

Returns:

  • (Boolean)


1169
1170
1171
# File 'app/models/ci/pipeline.rb', line 1169

def can_generate_codequality_reports?
  complete_and_has_reports?(Ci::JobArtifact.of_report_type(:codequality))
end

#cancel_async_on_job_failureObject



1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
# File 'app/models/ci/pipeline.rb', line 1419

def cancel_async_on_job_failure
  case auto_cancel_on_job_failure
  when 'none'
    # no-op
  when 'all'
    ::Ci::UserCancelPipelineWorker.perform_async(id, id, user.id)
  else
    raise ArgumentError,
      "Unknown auto_cancel_on_job_failure value: #{auto_cancel_on_job_failure}"
  end
end

#cancelable?Boolean

Returns:

  • (Boolean)


717
718
719
# File 'app/models/ci/pipeline.rb', line 717

def cancelable?
  cancelable_statuses.any?
end

#child?Boolean

Returns:

  • (Boolean)


1087
1088
1089
1090
# File 'app/models/ci/pipeline.rb', line 1087

def child?
  parent_pipeline? && # child pipelines have `parent_pipeline` source
    parent_pipeline.present?
end

#cluster_agent_authorizationsObject



1383
1384
1385
1386
1387
# File 'app/models/ci/pipeline.rb', line 1383

def cluster_agent_authorizations
  strong_memoize(:cluster_agent_authorizations) do
    ::Clusters::Agents::Authorizations::CiAccess::Finder.new(project).execute
  end
end

#codequality_reportsObject



1195
1196
1197
1198
1199
1200
1201
# File 'app/models/ci/pipeline.rb', line 1195

def codequality_reports
  Gitlab::Ci::Reports::CodequalityReports.new.tap do |codequality_reports|
    latest_report_builds(Ci::JobArtifact.of_report_type(:codequality)).each do |build|
      build.collect_codequality_reports!(codequality_reports)
    end
  end
end

#commitObject

NOTE: This is loaded lazily and will never be nil, even if the commit cannot be found.

Use constructs like: ‘pipeline.commit.present?`



705
706
707
# File 'app/models/ci/pipeline.rb', line 705

def commit
  @commit ||= Commit.lazy(project, sha)
end

#complete_and_has_reports?(reports_scope) ⇒ Boolean

Returns:

  • (Boolean)


1145
1146
1147
1148
1149
1150
1151
# File 'app/models/ci/pipeline.rb', line 1145

def complete_and_has_reports?(reports_scope)
  if Feature.enabled?(:mr_show_reports_immediately, project, type: :development)
    latest_report_builds(reports_scope).exists?
  else
    complete? && has_reports?(reports_scope)
  end
end

#complete_hierarchy_countObject

Applies to all parent-child and multi-project pipelines



1075
1076
1077
# File 'app/models/ci/pipeline.rb', line 1075

def complete_hierarchy_count
  upstream_root.self_and_downstreams.count
end

#complete_or_manual_and_has_reports?(reports_scope) ⇒ Boolean

Returns:

  • (Boolean)


1153
1154
1155
1156
1157
1158
1159
# File 'app/models/ci/pipeline.rb', line 1153

def complete_or_manual_and_has_reports?(reports_scope)
  if Feature.enabled?(:mr_show_reports_immediately, project, type: :development)
    latest_report_builds(reports_scope).exists?
  else
    complete_or_manual? && has_reports?(reports_scope)
  end
end

#coverageObject



753
754
755
756
# File 'app/models/ci/pipeline.rb', line 753

def coverage
  coverage_array = latest_statuses.map(&:coverage).compact
  coverage_array.sum / coverage_array.size if coverage_array.size >= 1
end

#created_successfully?Boolean

Returns:

  • (Boolean)


1096
1097
1098
# File 'app/models/ci/pipeline.rb', line 1096

def created_successfully?
  persisted? && failure_reason.blank?
end

#dangling?Boolean

Returns:

  • (Boolean)


1336
1337
1338
# File 'app/models/ci/pipeline.rb', line 1336

def dangling?
  Enums::Ci::Pipeline.dangling_sources.key?(source.to_sym)
end

#default_branch?Boolean

Returns:

  • (Boolean)


1264
1265
1266
# File 'app/models/ci/pipeline.rb', line 1264

def default_branch?
  ref == project.default_branch
end

#detached_merge_request_pipeline?Boolean

Returns:

  • (Boolean)


1276
1277
1278
# File 'app/models/ci/pipeline.rb', line 1276

def detached_merge_request_pipeline?
  merge_request? && target_sha.nil?
end

#detailed_status(current_user) ⇒ Object



1104
1105
1106
1107
1108
# File 'app/models/ci/pipeline.rb', line 1104

def detailed_status(current_user)
  Gitlab::Ci::Status::Pipeline::Factory
    .new(self.present, current_user)
    .fabricate!
end

#distinct_tags_countObject



622
623
624
# File 'app/models/ci/pipeline.rb', line 622

def distinct_tags_count
  Ci::Tagging.where(taggable: builds).count('distinct(tag_id)')
end

#ensure_ci_ref!Object



1354
1355
1356
# File 'app/models/ci/pipeline.rb', line 1354

def ensure_ci_ref!
  self.ci_ref = Ci::Ref.ensure_for(self)
end

#ensure_persistent_refObject



1358
1359
1360
1361
1362
# File 'app/models/ci/pipeline.rb', line 1358

def ensure_persistent_ref
  return if persistent_ref.exist?

  persistent_ref.create
end

#ensure_scheduling_type!Object

Set scheduling type of processables if they were created before scheduling_type data was deployed (gitlab.com/gitlab-org/gitlab/-/merge_requests/22246).



1350
1351
1352
# File 'app/models/ci/pipeline.rb', line 1350

def ensure_scheduling_type!
  processables.populate_scheduling_type!
end

#environments_in_self_and_project_descendants(deployment_status: nil) ⇒ Object



1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
# File 'app/models/ci/pipeline.rb', line 1012

def environments_in_self_and_project_descendants(deployment_status: nil)
  # We limit to 100 unique environments for application safety.
  # See: https://gitlab.com/gitlab-org/gitlab/-/issues/340781#note_699114700
  expanded_environment_names =
    jobs_in_self_and_project_descendants.joins(:metadata)
                                  .where.not(Ci::BuildMetadata.table_name => { expanded_environment_name: nil })
                                  .distinct("#{Ci::BuildMetadata.quoted_table_name}.expanded_environment_name")
                                  .limit(100)
                                  .pluck(:expanded_environment_name)

  Environment.where(project: project, name: expanded_environment_names).with_deployment(sha, status: deployment_status)
end

#error_messagesObject

We can’t use ‘messages.error` scope here because messages should also be read when the pipeline is not persisted. Using the scope will return no results as it would query persisted data.



851
852
853
# File 'app/models/ci/pipeline.rb', line 851

def error_messages
  messages.select(&:error?)
end

#external_pull_request?Boolean

Returns:

  • (Boolean)


1272
1273
1274
# File 'app/models/ci/pipeline.rb', line 1272

def external_pull_request?
  external_pull_request_id.present?
end

#filtered_as_empty?Boolean

Returns:

  • (Boolean)


1100
1101
1102
# File 'app/models/ci/pipeline.rb', line 1100

def filtered_as_empty?
  filtered_by_rules? || filtered_by_workflow_rules?
end

#find_job_with_archive_artifacts(name) ⇒ Object



1110
1111
1112
# File 'app/models/ci/pipeline.rb', line 1110

def find_job_with_archive_artifacts(name)
  builds.latest.with_downloadable_artifacts.find_by_name(name)
end

#freeze_period?Boolean

Returns:

  • (Boolean)


801
802
803
804
805
# File 'app/models/ci/pipeline.rb', line 801

def freeze_period?
  strong_memoize(:freeze_period) do
    project.freeze_periods.any?(&:active?)
  end
end

#full_error_messagesObject



1316
1317
1318
# File 'app/models/ci/pipeline.rb', line 1316

def full_error_messages
  errors ? errors.full_messages.to_sentence : ""
end

#git_author_emailObject



651
652
653
654
655
# File 'app/models/ci/pipeline.rb', line 651

def git_author_email
  strong_memoize(:git_author_email) do
    commit.try(:author_email)
  end
end

#git_author_full_textObject



657
658
659
660
661
# File 'app/models/ci/pipeline.rb', line 657

def git_author_full_text
  strong_memoize(:git_author_full_text) do
    commit.try(:author_full_text)
  end
end

#git_author_nameObject



645
646
647
648
649
# File 'app/models/ci/pipeline.rb', line 645

def git_author_name
  strong_memoize(:git_author_name) do
    commit.try(:author_name)
  end
end

#git_commit_descriptionObject



681
682
683
684
685
# File 'app/models/ci/pipeline.rb', line 681

def git_commit_description
  strong_memoize(:git_commit_description) do
    commit.try(:description)
  end
end

#git_commit_full_titleObject



675
676
677
678
679
# File 'app/models/ci/pipeline.rb', line 675

def git_commit_full_title
  strong_memoize(:git_commit_full_title) do
    commit.try(:full_title)
  end
end

#git_commit_messageObject



663
664
665
666
667
# File 'app/models/ci/pipeline.rb', line 663

def git_commit_message
  strong_memoize(:git_commit_message) do
    commit.try(:message)
  end
end

#git_commit_timestampObject



687
688
689
690
691
# File 'app/models/ci/pipeline.rb', line 687

def git_commit_timestamp
  strong_memoize(:git_commit_timestamp) do
    commit.try(:timestamp)
  end
end

#git_commit_titleObject



669
670
671
672
673
# File 'app/models/ci/pipeline.rb', line 669

def git_commit_title
  strong_memoize(:git_commit_title) do
    commit.try(:title)
  end
end

#has_archive_artifacts?Boolean

Returns:

  • (Boolean)


1211
1212
1213
# File 'app/models/ci/pipeline.rb', line 1211

def has_archive_artifacts?
  complete? && builds.latest.with_existing_job_artifacts(Ci::JobArtifact.archive.or(Ci::JobArtifact.)).exists?
end

#has_codequality_mr_diff_report?Boolean

Returns:

  • (Boolean)


1165
1166
1167
# File 'app/models/ci/pipeline.rb', line 1165

def has_codequality_mr_diff_report?
  pipeline_artifacts&.report_exists?(:code_quality_mr_diff)
end

#has_coverage_reports?Boolean

Returns:

  • (Boolean)


1161
1162
1163
# File 'app/models/ci/pipeline.rb', line 1161

def has_coverage_reports?
  pipeline_artifacts&.report_exists?(:code_coverage)
end

#has_erasable_artifacts?Boolean

Returns:

  • (Boolean)


1219
1220
1221
# File 'app/models/ci/pipeline.rb', line 1219

def has_erasable_artifacts?
  complete? && builds.latest.with_erasable_artifacts.exists?
end

#has_exposed_artifacts?Boolean

Returns:

  • (Boolean)


1215
1216
1217
# File 'app/models/ci/pipeline.rb', line 1215

def has_exposed_artifacts?
  complete? && builds.latest.with_exposed_artifacts.exists?
end

#has_kubernetes_active?Boolean

Returns:

  • (Boolean)


795
796
797
798
799
# File 'app/models/ci/pipeline.rb', line 795

def has_kubernetes_active?
  strong_memoize(:has_kubernetes_active) do
    project.deployment_platform&.active?
  end
end

#has_reports?(reports_scope) ⇒ Boolean

Returns:

  • (Boolean)


1141
1142
1143
# File 'app/models/ci/pipeline.rb', line 1141

def has_reports?(reports_scope)
  latest_report_builds(reports_scope).exists?
end

#has_test_reports?Boolean

Returns:

  • (Boolean)


1389
1390
1391
1392
1393
# File 'app/models/ci/pipeline.rb', line 1389

def has_test_reports?
  strong_memoize(:has_test_reports) do
    has_reports?(::Ci::JobArtifact.of_report_type(:test))
  end
end

#has_warnings?Boolean

Returns:

  • (Boolean)


807
808
809
# File 'app/models/ci/pipeline.rb', line 807

def has_warnings?
  number_of_warnings > 0
end

#has_yaml_errors?Boolean

Returns:

  • (Boolean)


829
830
831
# File 'app/models/ci/pipeline.rb', line 829

def has_yaml_errors?
  yaml_errors.present?
end

#jobs_git_refObject

This is used to retain access to the method defined by ‘Ci::HasRef` before being overridden in this class.



55
# File 'app/models/ci/pipeline.rb', line 55

alias_method :jobs_git_ref, :git_ref

#jobs_in_self_and_project_descendantsObject



1008
1009
1010
# File 'app/models/ci/pipeline.rb', line 1008

def jobs_in_self_and_project_descendants
  Ci::Processable.in_partition(self).latest.where(pipeline: self_and_project_descendants)
end

#latest?Boolean

Returns:

  • (Boolean)


742
743
744
745
746
747
# File 'app/models/ci/pipeline.rb', line 742

def latest?
  return false unless git_ref && commit.present?
  return false if lazy_ref_commit.nil?

  lazy_ref_commit.id == commit.id
end

#latest_builds_with_artifactsObject



1114
1115
1116
1117
1118
1119
# File 'app/models/ci/pipeline.rb', line 1114

def latest_builds_with_artifacts
  # We purposely cast the builds to an Array here. Because we always use the
  # rows if there are more than 0 this prevents us from having to run two
  # queries: one to get the count and one to get the rows.
  @latest_builds_with_artifacts ||= builds.latest.with_artifacts_not_expired.to_a
end

#latest_report_artifactsObject

This batch loads the latest reports for each CI job artifact type (e.g. sast, dast, etc.) in a single SQL query to eliminate the need to do N different ‘job_artifacts.where(file_type: X).last` calls.

Return a hash of file type => array of 1 job artifact



783
784
785
786
787
788
789
790
791
792
793
# File 'app/models/ci/pipeline.rb', line 783

def latest_report_artifacts
  ::Gitlab::SafeRequestStore.fetch("pipeline:#{self.id}:latest_report_artifacts") do
    ::Ci::JobArtifact.where(
      id: job_artifacts.all_reports
        .select("max(#{Ci::JobArtifact.quoted_table_name}.id) as id")
        .group(:file_type)
    )
      .preload(:job)
      .group_by(&:file_type)
  end
end

#latest_report_builds(reports_scope = ::Ci::JobArtifact.all_reports) ⇒ Object



1121
1122
1123
# File 'app/models/ci/pipeline.rb', line 1121

def latest_report_builds(reports_scope = ::Ci::JobArtifact.all_reports)
  builds.latest.with_artifacts(reports_scope)
end

#latest_report_builds_in_self_and_project_descendants(reports_scope = ::Ci::JobArtifact.all_reports) ⇒ Object



1129
1130
1131
# File 'app/models/ci/pipeline.rb', line 1129

def latest_report_builds_in_self_and_project_descendants(reports_scope = ::Ci::JobArtifact.all_reports)
  builds_in_self_and_project_descendants.with_artifacts(reports_scope)
end

#latest_test_report_buildsObject



1125
1126
1127
# File 'app/models/ci/pipeline.rb', line 1125

def latest_test_report_builds
  latest_report_builds(Ci::JobArtifact.of_report_type(:test)).preload(:project, :metadata, job_artifacts: :artifact_report)
end

#lazy_ref_commitObject

rubocop: enable CodeReuse/ServiceClass



732
733
734
735
736
737
738
739
740
# File 'app/models/ci/pipeline.rb', line 732

def lazy_ref_commit
  BatchLoader.for(ref).batch(key: project.id) do |refs, loader|
    next unless project.repository_exists?

    project.repository.list_commits_by_ref_name(refs).then do |commits|
      commits.each { |key, commit| loader.call(key, commits[key]) }
    end
  end
end

#legacy_detached_merge_request_pipeline?Boolean

Returns:

  • (Boolean)


1280
1281
1282
# File 'app/models/ci/pipeline.rb', line 1280

def legacy_detached_merge_request_pipeline?
  detached_merge_request_pipeline? && !merge_request_ref?
end

#legacy_triggerObject



912
913
914
# File 'app/models/ci/pipeline.rb', line 912

def legacy_trigger
  strong_memoize(:legacy_trigger) { trigger_requests.first }
end

#matches_sha_or_source_sha?(sha) ⇒ Boolean

Returns:

  • (Boolean)


1292
1293
1294
# File 'app/models/ci/pipeline.rb', line 1292

def matches_sha_or_source_sha?(sha)
  self.sha == sha || self.source_sha == sha
end

#merge_request?Boolean

Returns:

  • (Boolean)


1268
1269
1270
# File 'app/models/ci/pipeline.rb', line 1268

def merge_request?
  merge_request_id.present? && merge_request.present?
end

#merge_request_diffObject



1405
1406
1407
1408
1409
# File 'app/models/ci/pipeline.rb', line 1405

def merge_request_diff
  return unless merge_request?

  merge_request.merge_request_diff_for(merge_request_diff_sha)
end

#merge_request_event_typeObject



1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
# File 'app/models/ci/pipeline.rb', line 1320

def merge_request_event_type
  return unless merge_request?

  strong_memoize(:merge_request_event_type) do
    if merged_result_pipeline?
      :merged_result
    elsif detached_merge_request_pipeline?
      :detached
    end
  end
end

#merge_request_ref?Boolean

Returns:

  • (Boolean)


1288
1289
1290
# File 'app/models/ci/pipeline.rb', line 1288

def merge_request_ref?
  MergeRequest.merge_request_ref?(ref)
end

#merge_train_pipeline?Boolean

EE-only

Returns:

  • (Boolean)


1375
1376
1377
# File 'app/models/ci/pipeline.rb', line 1375

def merge_train_pipeline?
  false
end

#merged_result_pipeline?Boolean

Returns:

  • (Boolean)


1284
1285
1286
# File 'app/models/ci/pipeline.rb', line 1284

def merged_result_pipeline?
  merge_request? && target_sha.present?
end

#modified_pathsObject

Returns the modified paths.

The returned value is

  • Array: List of modified paths that should be evaluated

  • nil: Modified path can not be evaluated



1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
# File 'app/models/ci/pipeline.rb', line 1234

def modified_paths
  strong_memoize(:modified_paths) do
    if merge_request?
      merge_request.modified_paths
    elsif branch_updated?
      push_details.modified_paths
    elsif external_pull_request?
      external_pull_request.modified_paths
    end
  end
end

#modified_paths_since(compare_to_sha) ⇒ Object



1246
1247
1248
1249
1250
# File 'app/models/ci/pipeline.rb', line 1246

def modified_paths_since(compare_to_sha)
  strong_memoize_with(:modified_paths_since, compare_to_sha) do
    project.repository.diff_stats(project.repository.merge_base(compare_to_sha, sha), sha).paths
  end
end

#needs_processing?Boolean

Returns:

  • (Boolean)


822
823
824
825
826
827
# File 'app/models/ci/pipeline.rb', line 822

def needs_processing?
  statuses
    .where(processed: [false, nil])
    .latest
    .exists?
end

#notesObject



880
881
882
# File 'app/models/ci/pipeline.rb', line 880

def notes
  project.notes.for_commit_id(sha)
end

#notes=(notes_to_save) ⇒ Object

Manually set the notes for a Ci::Pipeline There is no ActiveRecord relation between Ci::Pipeline and notes as they are related to a commit sha. This method helps importing them using the Gitlab::ImportExport::Project::RelationFactory class.



865
866
867
868
869
870
871
872
873
874
875
876
877
878
# File 'app/models/ci/pipeline.rb', line 865

def notes=(notes_to_save)
  notes_to_save.reject! do |note_to_save|
    notes.any? do |note|
      [note_to_save.note, note_to_save.created_at.to_i] == [note.note, note.created_at.to_i]
    end
  end

  notes_to_save.each do |note|
    note[:id] = nil
    note[:commit_id] = sha
    note[:noteable_id] = self['id']
    note.save!
  end
end

#number_of_warningsObject



811
812
813
814
815
816
817
818
819
820
# File 'app/models/ci/pipeline.rb', line 811

def number_of_warnings
  BatchLoader.for(id).batch(default_value: 0) do |pipeline_ids, loader|
    ::CommitStatus.where(commit_id: pipeline_ids)
      .latest
      .failed_but_allowed
      .group(:commit_id)
      .count
      .each { |id, amount| loader.call(id, amount) }
  end
end

#open_merge_requests_refsObject

We cannot use ‘all_merge_requests`, due to race condition This returns a list of at most 4 open MRs



974
975
976
977
978
979
980
981
982
983
984
985
# File 'app/models/ci/pipeline.rb', line 974

def open_merge_requests_refs
  strong_memoize(:open_merge_requests_refs) do
    # We ensure that triggering user can actually read the pipeline
    related_merge_requests
      .opened
      .limit(MAX_OPEN_MERGE_REQUESTS_REFS)
      .order(id: :desc)
      .preload(:target_project)
      .select { |mr| can?(user, :read_merge_request, mr) }
      .map { |mr| mr.to_reference(project, full: true) }
  end
end

#parent?Boolean

Returns:

  • (Boolean)


1092
1093
1094
# File 'app/models/ci/pipeline.rb', line 1092

def parent?
  child_pipelines.exists?
end

#persisted_variablesObject



920
921
922
923
924
925
926
927
# File 'app/models/ci/pipeline.rb', line 920

def persisted_variables
  Gitlab::Ci::Variables::Collection.new.tap do |variables|
    break variables unless persisted?

    variables.append(key: 'CI_PIPELINE_ID', value: id.to_s)
    variables.append(key: 'CI_PIPELINE_URL', value: Gitlab::Routing.url_helpers.project_pipeline_url(project, self))
  end
end

#persistent_refObject



1332
1333
1334
# File 'app/models/ci/pipeline.rb', line 1332

def persistent_ref
  @persistent_ref ||= PersistentRef.new(pipeline: self)
end

#protected_ref?Boolean

rubocop: enable Metrics/CyclomaticComplexity

Returns:

  • (Boolean)


908
909
910
# File 'app/models/ci/pipeline.rb', line 908

def protected_ref?
  strong_memoize(:protected_ref) { project.protected_for?(git_ref) }
end

#queued_durationObject



929
930
931
932
933
934
# File 'app/models/ci/pipeline.rb', line 929

def queued_duration
  return unless started_at

  seconds = (started_at - created_at).to_i
  seconds unless seconds == 0
end

#ref_exists?Boolean

Returns:

  • (Boolean)


631
632
633
634
635
# File 'app/models/ci/pipeline.rb', line 631

def ref_exists?
  project.repository.ref_exists?(git_ref)
rescue Gitlab::Git::Repository::NoRepository
  false
end

This returns a list of MRs that point to the same source project/branch



959
960
961
962
963
964
965
966
967
968
969
970
# File 'app/models/ci/pipeline.rb', line 959

def related_merge_requests
  if merge_request?
    # We look for all other MRs that this branch might be pointing to
    MergeRequest.where(
      source_project_id: merge_request.source_project_id,
      source_branch: merge_request.source_branch)
  else
    MergeRequest.where(
      source_project_id: project_id,
      source_branch: ref)
  end
end

#reset_source_bridge!(current_user) ⇒ Object

For dependent bridge jobs we reset the upstream bridge recursively to reflect that a downstream pipeline is running again



1366
1367
1368
1369
1370
1371
1372
# File 'app/models/ci/pipeline.rb', line 1366

def reset_source_bridge!(current_user)
  # break recursion when no source_pipeline bridge (first upstream pipeline)
  return unless bridge_waiting?
  return unless current_user.can?(:update_pipeline, source_bridge.pipeline)

  Ci::EnqueueJobService.new(source_bridge, current_user: current_user).execute(&:pending!) # rubocop:disable CodeReuse/ServiceClass
end

#retriedObject



749
750
751
# File 'app/models/ci/pipeline.rb', line 749

def retried
  @retried ||= (statuses.order(id: :desc) - latest_statuses)
end

#retry_failed(current_user) ⇒ Object

rubocop: disable CodeReuse/ServiceClass



726
727
728
729
# File 'app/models/ci/pipeline.rb', line 726

def retry_failed(current_user)
  Ci::RetryPipelineService.new(project, current_user)
    .execute(self)
end

#retryable?Boolean

Returns:

  • (Boolean)


713
714
715
# File 'app/models/ci/pipeline.rb', line 713

def retryable?
  retryable_builds.any?
end

#root_ancestorObject

Follow the parent-child relationships and return the top-level parent



1060
1061
1062
1063
1064
1065
1066
# File 'app/models/ci/pipeline.rb', line 1060

def root_ancestor
  return self unless child?

  object_hierarchy(project_condition: :same)
    .base_and_ancestors(hierarchy_order: :desc)
    .first
end

#same_family_pipeline_idsObject



987
988
989
990
991
# File 'app/models/ci/pipeline.rb', line 987

def same_family_pipeline_ids
  ::Gitlab::Ci::PipelineObjectHierarchy.new(
    self.class.default_scoped.where(id: root_ancestor), options: { project_condition: :same }
  ).base_and_descendants.select(:id)
end

#self_and_downstreamsObject

With multi-project and parent-child pipelines



1031
1032
1033
# File 'app/models/ci/pipeline.rb', line 1031

def self_and_downstreams
  object_hierarchy.base_and_descendants
end

#self_and_project_ancestorsObject

With only parent-child pipelines



1041
1042
1043
# File 'app/models/ci/pipeline.rb', line 1041

def self_and_project_ancestors
  object_hierarchy(project_condition: :same).base_and_ancestors
end

#self_and_project_descendantsObject

With only parent-child pipelines



1046
1047
1048
# File 'app/models/ci/pipeline.rb', line 1046

def self_and_project_descendants
  object_hierarchy(project_condition: :same).base_and_descendants
end

#self_and_project_descendants_complete?Boolean

Returns:

  • (Boolean)


1055
1056
1057
# File 'app/models/ci/pipeline.rb', line 1055

def self_and_project_descendants_complete?
  self_and_project_descendants.all?(&:complete?)
end

#self_and_upstreamsObject

With multi-project and parent-child pipelines



1026
1027
1028
# File 'app/models/ci/pipeline.rb', line 1026

def self_and_upstreams
  object_hierarchy.base_and_ancestors
end

#set_failed(failure_reason) ⇒ Object

Like #drop!, but does not persist the pipeline nor trigger any state machine callbacks.



843
844
845
846
# File 'app/models/ci/pipeline.rb', line 843

def set_failed(failure_reason)
  self.failure_reason = failure_reason.to_s
  self.status = 'failed'
end

#set_status(new_status) ⇒ Object

rubocop: disable Metrics/CyclomaticComplexity – breaking apart hurts readability



885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
# File 'app/models/ci/pipeline.rb', line 885

def set_status(new_status)
  retry_optimistic_lock(self, name: 'ci_pipeline_set_status') do
    case new_status
    when 'created' then nil
    when 'waiting_for_resource' then request_resource
    when 'preparing' then prepare
    when 'waiting_for_callback' then wait_for_callback
    when 'pending' then enqueue
    when 'running' then run
    when 'success' then succeed
    when 'failed' then drop
    when 'canceling' then start_cancel
    when 'canceled' then cancel
    when 'skipped' then skip
    when 'manual' then block
    when 'scheduled' then delay
    else
      raise Ci::HasStatus::UnknownStatusError, "Unknown status `#{new_status}`"
    end
  end
end

#short_shaObject



697
698
699
# File 'app/models/ci/pipeline.rb', line 697

def short_sha
  Ci::Pipeline.truncate_sha(sha)
end

#source_refObject



1300
1301
1302
1303
1304
1305
1306
# File 'app/models/ci/pipeline.rb', line 1300

def source_ref
  if merge_request?
    merge_request.source_branch
  else
    ref
  end
end

#source_ref_pathObject



1340
1341
1342
1343
1344
1345
1346
# File 'app/models/ci/pipeline.rb', line 1340

def source_ref_path
  if branch? || merge_request?
    Gitlab::Git::BRANCH_REF_PREFIX + source_ref.to_s
  elsif tag?
    Gitlab::Git::TAG_REF_PREFIX + source_ref.to_s
  end
end

#source_ref_slugObject



1308
1309
1310
# File 'app/models/ci/pipeline.rb', line 1308

def source_ref_slug
  Gitlab::Utils.slugify(source_ref.to_s)
end

#stage(name) ⇒ Object



1312
1313
1314
# File 'app/models/ci/pipeline.rb', line 1312

def stage(name)
  stages.find_by(name: name)
end

#stages_countObject



610
611
612
# File 'app/models/ci/pipeline.rb', line 610

def stages_count
  statuses.select(:stage).distinct.count
end

#stages_namesObject



626
627
628
629
# File 'app/models/ci/pipeline.rb', line 626

def stages_names
  statuses.order(:stage_idx).distinct
    .pluck(:stage, :stage_idx).map(&:first)
end

#stuck?Boolean

Returns:

  • (Boolean)


709
710
711
# File 'app/models/ci/pipeline.rb', line 709

def stuck?
  pending_builds.any?(&:stuck?)
end

#tags_countObject



618
619
620
# File 'app/models/ci/pipeline.rb', line 618

def tags_count
  Ci::Tagging.where(taggable: builds).count
end

#terraform_reportsObject



1203
1204
1205
1206
1207
1208
1209
# File 'app/models/ci/pipeline.rb', line 1203

def terraform_reports
  ::Gitlab::Ci::Reports::TerraformReports.new.tap do |terraform_reports|
    latest_report_builds(::Ci::JobArtifact.of_report_type(:terraform)).each do |build|
      build.collect_terraform_reports!(terraform_reports)
    end
  end
end

#test_report_summaryObject



1173
1174
1175
1176
1177
# File 'app/models/ci/pipeline.rb', line 1173

def test_report_summary
  strong_memoize(:test_report_summary) do
    Gitlab::Ci::Reports::TestReportSummary.new(latest_builds_report_results)
  end
end

#test_reportsObject



1179
1180
1181
1182
1183
1184
1185
# File 'app/models/ci/pipeline.rb', line 1179

def test_reports
  Gitlab::Ci::Reports::TestReport.new.tap do |test_reports|
    latest_test_report_builds.find_each do |build|
      build.collect_test_reports!(test_reports)
    end
  end
end

#top_level_worktree_pathsObject



1258
1259
1260
1261
1262
# File 'app/models/ci/pipeline.rb', line 1258

def top_level_worktree_paths
  strong_memoize(:top_level_worktree_paths) do
    project.repository.tree(sha).blobs.map(&:path)
  end
end

#total_sizeObject



614
615
616
# File 'app/models/ci/pipeline.rb', line 614

def total_size
  statuses.count(:id)
end

#triggered_by?(current_user) ⇒ Boolean

Returns:

  • (Boolean)


1296
1297
1298
# File 'app/models/ci/pipeline.rb', line 1296

def triggered_by?(current_user)
  user == current_user
end

#triggered_pipelines_with_preloadsObject



637
638
639
# File 'app/models/ci/pipeline.rb', line 637

def triggered_pipelines_with_preloads
  triggered_pipelines.preload(:source_job)
end

#update_builds_coverageObject



758
759
760
# File 'app/models/ci/pipeline.rb', line 758

def update_builds_coverage
  builds.with_coverage_regex.without_coverage.each(&:update_coverage)
end

#update_durationObject



936
937
938
939
940
# File 'app/models/ci/pipeline.rb', line 936

def update_duration
  return unless started_at

  self.duration = Gitlab::Ci::Pipeline::Duration.from_pipeline(self)
end

#upstream_and_all_downstreamsObject

With multi-project and parent-child pipelines



1036
1037
1038
# File 'app/models/ci/pipeline.rb', line 1036

def upstream_and_all_downstreams
  object_hierarchy.all_objects
end

#upstream_rootObject

Follow the upstream pipeline relationships, regardless of multi-project or parent-child, and return the top-level ancestor.



1070
1071
1072
# File 'app/models/ci/pipeline.rb', line 1070

def upstream_root
  @upstream_root ||= object_hierarchy.base_and_ancestors(hierarchy_order: :desc).first
end

#uses_needs?Boolean

Returns:

  • (Boolean)


606
607
608
# File 'app/models/ci/pipeline.rb', line 606

def uses_needs?
  processables.where(scheduling_type: :dag).any?
end

#valid_commit_shaObject



641
642
643
# File 'app/models/ci/pipeline.rb', line 641

def valid_commit_sha
  self.errors.add(:sha, "can't be 00000000 (branch removal)") if Gitlab::Git.blank_ref?(self.sha)
end

#variables_builderObject



916
917
918
# File 'app/models/ci/pipeline.rb', line 916

def variables_builder
  @variables_builder ||= ::Gitlab::Ci::Variables::Builder.new(self)
end

#warning_messages(limit: nil) ⇒ Object



855
856
857
858
859
# File 'app/models/ci/pipeline.rb', line 855

def warning_messages(limit: nil)
  messages.select(&:warning?).tap do |warnings|
    break warnings.take(limit) if limit
  end
end