Class: Ci::Pipeline

Constant Summary collapse

PROJECT_ROUTE_AND_NAMESPACE_ROUTE =
{
  project: [:project_feature, :route, { namespace: :route }]
}.freeze
CONFIG_EXTENSION =
'.gitlab-ci.yml'
DEFAULT_CONFIG_PATH =
CONFIG_EXTENSION
BridgeStatusError =
Class.new(StandardError)

Constants included from HasStatus

HasStatus::ACTIVE_STATUSES, HasStatus::AVAILABLE_STATUSES, HasStatus::BLOCKED_STATUS, HasStatus::COMPLETED_STATUSES, HasStatus::DEFAULT_STATUS, HasStatus::EXCLUDE_IGNORED_STATUSES, HasStatus::ORDERED_STATUSES, HasStatus::PASSED_WITH_WARNINGS_STATUSES, HasStatus::STARTED_STATUSES, HasStatus::STATUSES_ENUM, HasStatus::UnknownStatusError

Instance Attribute Summary collapse

Attributes included from Importable

#imported, #importing

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Gitlab::Ci::Model

model_name, table_name_prefix

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, project_init, scope_attrs, scope_usage

Methods included from Gitlab::Utils::StrongMemoize

#clear_memoization, #strong_memoize, #strong_memoized?

Methods included from Gitlab::OptimisticLocking

retry_lock

Methods included from Presentable

#present

Methods included from AfterCommitQueue

#run_after_commit, #run_after_commit_or_now

Methods included from HasStatus

#active?, #blocked?, #complete?, #started?

Methods inherited from ApplicationRecord

at_most, id_in, id_not_in, iid_in, pluck_primary_key, primary_key_in, safe_ensure_unique, safe_find_or_create_by, safe_find_or_create_by!, underscore, where_exists, with_fast_statement_timeout, without_order

Instance Attribute Details

#merged_yamlObject

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 CI YAML contents for linting purposes. There is an open issue to address this: gitlab.com/gitlab-org/gitlab/-/issues/259010


35
36
37
# File 'app/models/ci/pipeline.rb', line 35

def merged_yaml
  @merged_yaml
end

Class Method Details

.auto_devops_pipelines_completed_totalObject


424
425
426
# File 'app/models/ci/pipeline.rb', line 424

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


420
421
422
# File 'app/models/ci/pipeline.rb', line 420

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

.internal_sourcesObject


416
417
418
# File 'app/models/ci/pipeline.rb', line 416

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

.last_finished_for_ref_id(ci_ref_id) ⇒ Object


404
405
406
# File 'app/models/ci/pipeline.rb', line 404

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


362
363
364
# File 'app/models/ci/pipeline.rb', line 362

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.

375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
# File 'app/models/ci/pipeline.rb', line 375

def self.latest_pipeline_per_commit(commits, ref = nil)
  p1 = arel_table
  p2 = arel_table.alias

  # This LEFT JOIN will filter out all but the newest row for every
  # combination of (project_id, sha) or (project_id, sha, ref) if a ref is
  # given.
  cond = p1[:sha].eq(p2[:sha])
    .and(p1[:project_id].eq(p2[:project_id]))
    .and(p1[:id].lt(p2[:id]))

  cond = cond.and(p1[:ref].eq(p2[:ref])) if ref
  join = p1.join(p2, Arel::Nodes::OuterJoin).on(cond)

  relation = where(sha: commits)
    .where(p2[:id].eq(nil))
    .joins(join.join_sources)

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

  relation.each_with_object({}) do |pipeline, hash|
    hash[pipeline.sha] = pipeline
  end
end

.latest_running_for_ref(ref) ⇒ Object


358
359
360
# File 'app/models/ci/pipeline.rb', line 358

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

.latest_status(ref = nil) ⇒ Object


338
339
340
# File 'app/models/ci/pipeline.rb', line 338

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

.latest_successful_for_ref(ref) ⇒ Object


342
343
344
# File 'app/models/ci/pipeline.rb', line 342

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

.latest_successful_for_refs(refs) ⇒ Object


350
351
352
353
354
355
356
# File 'app/models/ci/pipeline.rb', line 350

def self.latest_successful_for_refs(refs)
  relation = newest_first(ref: refs).success

  relation.each_with_object({}) do |pipeline, hash|
    hash[pipeline.ref] ||= pipeline
  end
end

.latest_successful_for_sha(sha) ⇒ Object


346
347
348
# File 'app/models/ci/pipeline.rb', line 346

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

.latest_successful_ids_per_projectObject


400
401
402
# File 'app/models/ci/pipeline.rb', line 400

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

.newest_first(ref: nil, sha: nil, limit: 100) ⇒ 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 mutliple SHAs) to limit the list of pipelines to. limit - This limits a backlog search, default to 100.


325
326
327
328
329
330
331
332
333
334
335
336
# File 'app/models/ci/pipeline.rb', line 325

def self.newest_first(ref: nil, sha: nil, limit: 100)
  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

.total_durationObject


412
413
414
# File 'app/models/ci/pipeline.rb', line 412

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

.truncate_sha(sha) ⇒ Object


408
409
410
# File 'app/models/ci/pipeline.rb', line 408

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

Instance Method Details

#accessibility_reportsObject


968
969
970
971
972
973
974
# File 'app/models/ci/pipeline.rb', line 968

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

#add_error_message(content) ⇒ Object


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

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

#add_warning_message(content) ⇒ Object


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

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

#all_merge_requestsObject

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


842
843
844
845
846
847
848
849
850
# File 'app/models/ci/pipeline.rb', line 842

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


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

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

#all_worktree_pathsObject


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

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

#auto_cancel_running(pipeline, retries: nil) ⇒ Object


564
565
566
567
568
569
570
# File 'app/models/ci/pipeline.rb', line 564

def auto_cancel_running(pipeline, retries: nil)
  update(auto_canceled_by: pipeline)

  cancel_running(retries: retries) do |job|
    job.auto_canceled_by = pipeline
  end
end

#auto_canceled?Boolean

Returns:

  • (Boolean)

551
552
553
# File 'app/models/ci/pipeline.rb', line 551

def auto_canceled?
  canceled? && auto_canceled_by_id?
end

#base_and_ancestors(same_project: false) ⇒ Object


1135
1136
1137
1138
1139
1140
1141
# File 'app/models/ci/pipeline.rb', line 1135

def base_and_ancestors(same_project: false)
  # Without using `unscoped`, caller scope is also included into the query.
  # Using `unscoped` here will be redundant after Rails 6.1
  ::Gitlab::Ci::PipelineObjectHierarchy
    .new(self.class.unscoped.where(id: id), options: { same_project: same_project })
    .base_and_ancestors
end

#batch_lookup_report_artifact_for_file_type(file_type) ⇒ Object


614
615
616
617
618
619
620
# File 'app/models/ci/pipeline.rb', line 614

def batch_lookup_report_artifact_for_file_type(file_type)
  latest_report_artifacts
    .values_at(*::Ci::JobArtifact.associated_file_types_for(file_type.to_s))
    .flatten
    .compact
    .last
end

#before_shaObject


523
524
525
# File 'app/models/ci/pipeline.rb', line 523

def before_sha
  super || Gitlab::Git::BLANK_SHA
end

#branch_updated?Boolean

Returns:

  • (Boolean)

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

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

#bridge_triggered?Boolean

Returns:

  • (Boolean)

896
897
898
# File 'app/models/ci/pipeline.rb', line 896

def bridge_triggered?
  source_bridge.present?
end

#bridge_waiting?Boolean

Returns:

  • (Boolean)

900
901
902
# File 'app/models/ci/pipeline.rb', line 900

def bridge_waiting?
  source_bridge&.dependent?
end

#build_with_artifacts_in_self_and_descendants(name) ⇒ Object


868
869
870
871
872
873
# File 'app/models/ci/pipeline.rb', line 868

def build_with_artifacts_in_self_and_descendants(name)
  builds_in_self_and_descendants
    .ordered_by_pipeline # find job in hierarchical order
    .with_downloadable_artifacts
    .find_by_name(name)
end

#builds_in_self_and_descendantsObject


875
876
877
# File 'app/models/ci/pipeline.rb', line 875

def builds_in_self_and_descendants
  Ci::Build.latest.where(pipeline: self_and_descendants)
end

#builds_with_coverageObject


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

def builds_with_coverage
  builds.latest.with_coverage
end

#cacheable?Boolean

Returns:

  • (Boolean)

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

def cacheable?
  !dangling?
end

#can_generate_coverage_reports?Boolean

Returns:

  • (Boolean)

950
951
952
# File 'app/models/ci/pipeline.rb', line 950

def can_generate_coverage_reports?
  has_reports?(Ci::JobArtifact.coverage_reports)
end

#cancel_running(retries: nil) ⇒ Object


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

def cancel_running(retries: nil)
  retry_optimistic_lock(cancelable_statuses, retries) do |cancelable|
    cancelable.find_each do |job|
      yield(job) if block_given?
      job.cancel
    end
  end
end

#cancelable?Boolean

Returns:

  • (Boolean)

547
548
549
# File 'app/models/ci/pipeline.rb', line 547

def cancelable?
  cancelable_statuses.any?
end

#child?Boolean

Returns:

  • (Boolean)

904
905
906
907
# File 'app/models/ci/pipeline.rb', line 904

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

#codequality_reportsObject


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

def codequality_reports
  Gitlab::Ci::Reports::CodequalityReports.new.tap do |codequality_reports|
    latest_report_builds(Ci::JobArtifact.codequality_reports).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?`


535
536
537
# File 'app/models/ci/pipeline.rb', line 535

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

#config_pathObject

TODO: this logic is duplicate with Pipeline::Chain::Config::Content we should persist this is `ci_pipelines.config_path`


678
679
680
681
682
# File 'app/models/ci/pipeline.rb', line 678

def config_path
  return unless repository_source? || unknown_source?

  project.ci_config_path_or_default
end

#coverageObject


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

def coverage
  coverage_array = latest_statuses.map(&:coverage).compact
  if coverage_array.size >= 1
    '%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
  end
end

#coverage_reportsObject


976
977
978
979
980
981
982
# File 'app/models/ci/pipeline.rb', line 976

def coverage_reports
  Gitlab::Ci::Reports::CoverageReports.new.tap do |coverage_reports|
    latest_report_builds(Ci::JobArtifact.coverage_reports).each do |build|
      build.collect_coverage_reports!(coverage_reports)
    end
  end
end

#created_successfully?Boolean

Returns:

  • (Boolean)

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

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

#dangling?Boolean

Returns:

  • (Boolean)

1113
1114
1115
# File 'app/models/ci/pipeline.rb', line 1113

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

#default_branch?Boolean

Returns:

  • (Boolean)

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

def default_branch?
  ref == project.default_branch
end

#detached_merge_request_pipeline?Boolean

Returns:

  • (Boolean)

1049
1050
1051
# File 'app/models/ci/pipeline.rb', line 1049

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

#detailed_status(current_user) ⇒ Object


917
918
919
920
921
# File 'app/models/ci/pipeline.rb', line 917

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

#ensure_ci_ref!Object


1131
1132
1133
# File 'app/models/ci/pipeline.rb', line 1131

def ensure_ci_ref!
  self.ci_ref = Ci::Ref.ensure_for(self)
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).


1127
1128
1129
# File 'app/models/ci/pipeline.rb', line 1127

def ensure_scheduling_type!
  processables.populate_scheduling_type!
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.


699
700
701
# File 'app/models/ci/pipeline.rb', line 699

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

#execute_hooksObject


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

def execute_hooks
  data = pipeline_data
  project.execute_hooks(data, :pipeline_hooks)
  project.execute_services(data, :pipeline_hooks)
end

#find_job_with_archive_artifacts(name) ⇒ Object


923
924
925
# File 'app/models/ci/pipeline.rb', line 923

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

#find_stage_by_name!(name) ⇒ Object


1085
1086
1087
# File 'app/models/ci/pipeline.rb', line 1085

def find_stage_by_name!(name)
  stages.find_by!(name: name)
end

#freeze_period?Boolean

Returns:

  • (Boolean)

650
651
652
# File 'app/models/ci/pipeline.rb', line 650

def freeze_period?
  Ci::FreezePeriodStatus.new(project: project).execute
end

#full_error_messagesObject


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

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

#git_author_emailObject


487
488
489
490
491
# File 'app/models/ci/pipeline.rb', line 487

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

#git_author_nameObject


481
482
483
484
485
# File 'app/models/ci/pipeline.rb', line 481

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

#git_commit_descriptionObject


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

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

#git_commit_full_titleObject


505
506
507
508
509
# File 'app/models/ci/pipeline.rb', line 505

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

#git_commit_messageObject


493
494
495
496
497
# File 'app/models/ci/pipeline.rb', line 493

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

#git_commit_timestampObject


517
518
519
520
521
# File 'app/models/ci/pipeline.rb', line 517

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

#git_commit_titleObject


499
500
501
502
503
# File 'app/models/ci/pipeline.rb', line 499

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

#has_archive_artifacts?Boolean

Returns:

  • (Boolean)

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

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

#has_coverage_reports?Boolean

Returns:

  • (Boolean)

946
947
948
# File 'app/models/ci/pipeline.rb', line 946

def has_coverage_reports?
  pipeline_artifacts&.has_code_coverage?
end

#has_exposed_artifacts?Boolean

Returns:

  • (Boolean)

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

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

#has_kubernetes_active?Boolean

Returns:

  • (Boolean)

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

def has_kubernetes_active?
  project.deployment_platform&.active?
end

#has_reports?(reports_scope) ⇒ Boolean

Returns:

  • (Boolean)

942
943
944
# File 'app/models/ci/pipeline.rb', line 942

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

#has_warnings?Boolean

Returns:

  • (Boolean)

654
655
656
# File 'app/models/ci/pipeline.rb', line 654

def has_warnings?
  number_of_warnings > 0
end

#has_yaml_errors?Boolean

Returns:

  • (Boolean)

684
685
686
# File 'app/models/ci/pipeline.rb', line 684

def has_yaml_errors?
  yaml_errors.present?
end

#latest?Boolean

Returns:

  • (Boolean)

591
592
593
594
595
596
597
598
599
600
601
# File 'app/models/ci/pipeline.rb', line 591

def latest?
  return false unless git_ref && commit.present?

  unless ::Gitlab::Ci::Features.pipeline_latest?
    return project.commit(git_ref) == commit
  end

  return false if lazy_ref_commit.nil?

  lazy_ref_commit.id == commit.id
end

#latest_builds_with_artifactsObject


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

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


628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
# File 'app/models/ci/pipeline.rb', line 628

def latest_report_artifacts
  ::Gitlab::SafeRequestStore.fetch("pipeline:#{self.id}:latest_report_artifacts") do
    # Note we use read_attribute(:project_id) to read the project
    # ID instead of self.project_id. The latter appears to load
    # the Project model. This extra filter doesn't appear to
    # affect query plan but included to ensure we don't leak the
    # wrong informaiton.
    ::Ci::JobArtifact.where(
      id: job_artifacts.with_reports
        .select('max(ci_job_artifacts.id) as id')
        .where(project_id: self.read_attribute(:project_id))
        .group(:file_type)
    )
      .preload(:job)
      .group_by(&:file_type)
  end
end

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


934
935
936
# File 'app/models/ci/pipeline.rb', line 934

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

#lazy_ref_commitObject

rubocop: enable CodeReuse/ServiceClass


579
580
581
582
583
584
585
586
587
588
589
# File 'app/models/ci/pipeline.rb', line 579

def lazy_ref_commit
  return unless ::Gitlab::Ci::Features.pipeline_latest?

  BatchLoader.for(ref).batch 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)

1053
1054
1055
# File 'app/models/ci/pipeline.rb', line 1053

def legacy_detached_merge_request_pipeline?
  detached_merge_request_pipeline? && !merge_request_ref?
end

#legacy_stage(name) ⇒ Object


441
442
443
444
# File 'app/models/ci/pipeline.rb', line 441

def legacy_stage(name)
  stage = Ci::LegacyStage.new(self, name: name)
  stage unless stage.statuses_count == 0
end

#legacy_stagesObject

TODO: Remove usage of this method in templates


471
472
473
# File 'app/models/ci/pipeline.rb', line 471

def legacy_stages
  legacy_stages_using_composite_status
end

#legacy_stages_using_composite_statusObject


452
453
454
455
456
457
458
459
460
461
462
463
464
# File 'app/models/ci/pipeline.rb', line 452

def legacy_stages_using_composite_status
  stages = latest_statuses_ordered_by_stage.group_by(&:stage)

  stages.map do |stage_name, jobs|
    composite_status = Gitlab::Ci::Status::Composite
      .new(jobs)

    Ci::LegacyStage.new(self,
      name: stage_name,
      status: composite_status.status,
      warnings: composite_status.warnings?)
  end
end

#legacy_triggerObject


751
752
753
# File 'app/models/ci/pipeline.rb', line 751

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

#matches_sha_or_source_sha?(sha) ⇒ Boolean

Returns:

  • (Boolean)

1065
1066
1067
# File 'app/models/ci/pipeline.rb', line 1065

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

#merge_request?Boolean

Returns:

  • (Boolean)

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

def merge_request?
  merge_request_id.present?
end

#merge_request_event_typeObject


1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
# File 'app/models/ci/pipeline.rb', line 1093

def merge_request_event_type
  return unless merge_request?

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

#merge_request_pipeline?Boolean

Returns:

  • (Boolean)

1057
1058
1059
# File 'app/models/ci/pipeline.rb', line 1057

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

#merge_request_ref?Boolean

Returns:

  • (Boolean)

1061
1062
1063
# File 'app/models/ci/pipeline.rb', line 1061

def merge_request_ref?
  MergeRequest.merge_request_ref?(ref)
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


1019
1020
1021
1022
1023
1024
1025
1026
1027
# File 'app/models/ci/pipeline.rb', line 1019

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

#needs_processing?Boolean

Returns:

  • (Boolean)

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

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

#notesObject


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

def notes
  project.notes.for_commit_id(sha)
end

#notes=(notes) ⇒ 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.


713
714
715
716
717
718
719
720
# File 'app/models/ci/pipeline.rb', line 713

def notes=(notes)
  notes.each do |note|
    note[:id] = nil
    note[:commit_id] = sha
    note[:noteable_id] = self['id']
    note.save!
  end
end

#number_of_warningsObject


658
659
660
661
662
663
664
665
666
667
# File 'app/models/ci/pipeline.rb', line 658

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

#parent?Boolean

Returns:

  • (Boolean)

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

def parent?
  child_pipelines.exists?
end

#persisted_variablesObject


755
756
757
758
759
760
761
762
# File 'app/models/ci/pipeline.rb', line 755

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


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

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

#predefined_commit_variablesObject


798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
# File 'app/models/ci/pipeline.rb', line 798

def predefined_commit_variables
  Gitlab::Ci::Variables::Collection.new.tap do |variables|
    variables.append(key: 'CI_COMMIT_SHA', value: sha)
    variables.append(key: 'CI_COMMIT_SHORT_SHA', value: short_sha)
    variables.append(key: 'CI_COMMIT_BEFORE_SHA', value: before_sha)
    variables.append(key: 'CI_COMMIT_REF_NAME', value: source_ref)
    variables.append(key: 'CI_COMMIT_REF_SLUG', value: source_ref_slug)
    variables.append(key: 'CI_COMMIT_BRANCH', value: ref) if branch?
    variables.append(key: 'CI_COMMIT_TAG', value: ref) if tag?
    variables.append(key: 'CI_COMMIT_MESSAGE', value: git_commit_message.to_s)
    variables.append(key: 'CI_COMMIT_TITLE', value: git_commit_full_title.to_s)
    variables.append(key: 'CI_COMMIT_DESCRIPTION', value: git_commit_description.to_s)
    variables.append(key: 'CI_COMMIT_REF_PROTECTED', value: (!!protected_ref?).to_s)
    variables.append(key: 'CI_COMMIT_TIMESTAMP', value: git_commit_timestamp.to_s)

    # legacy variables
    variables.append(key: 'CI_BUILD_REF', value: sha)
    variables.append(key: 'CI_BUILD_BEFORE_SHA', value: before_sha)
    variables.append(key: 'CI_BUILD_REF_NAME', value: source_ref)
    variables.append(key: 'CI_BUILD_REF_SLUG', value: source_ref_slug)
    variables.append(key: 'CI_BUILD_TAG', value: ref) if tag?
  end
end

#predefined_variablesObject


764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
# File 'app/models/ci/pipeline.rb', line 764

def predefined_variables
  Gitlab::Ci::Variables::Collection.new.tap do |variables|
    variables.append(key: 'CI_PIPELINE_IID', value: iid.to_s)
    variables.append(key: 'CI_PIPELINE_SOURCE', value: source.to_s)

    variables.append(key: 'CI_CONFIG_PATH', value: config_path)

    variables.concat(predefined_commit_variables)

    if merge_request?
      variables.append(key: 'CI_MERGE_REQUEST_EVENT_TYPE', value: merge_request_event_type.to_s)
      variables.append(key: 'CI_MERGE_REQUEST_SOURCE_BRANCH_SHA', value: source_sha.to_s)
      variables.append(key: 'CI_MERGE_REQUEST_TARGET_BRANCH_SHA', value: target_sha.to_s)

      if Feature.enabled?(:ci_mr_diff_variables, project)
        diff = self.merge_request_diff
        if diff.present?
          variables.append(key: 'CI_MERGE_REQUEST_DIFF_ID', value: diff.id.to_s)
          variables.append(key: 'CI_MERGE_REQUEST_DIFF_BASE_SHA', value: diff.base_commit_sha)
        end
      end

      variables.concat(merge_request.predefined_variables)
    end

    variables.append(key: 'CI_KUBERNETES_ACTIVE', value: 'true') if has_kubernetes_active?
    variables.append(key: 'CI_DEPLOY_FREEZE', value: 'true') if freeze_period?

    if external_pull_request_event? && external_pull_request
      variables.concat(external_pull_request.predefined_variables)
    end
  end
end

#protected_ref?Boolean

Returns:

  • (Boolean)

747
748
749
# File 'app/models/ci/pipeline.rb', line 747

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

#queued_durationObject


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

def queued_duration
  return unless started_at

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

#ref_exists?Boolean

Returns:

  • (Boolean)

446
447
448
449
450
# File 'app/models/ci/pipeline.rb', line 446

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

#reset_ancestor_bridges!Object

We need `base_and_ancestors` in a specific order to “break” when needed. If we use `find_each`, then the order is broken. rubocop:disable Rails/FindEach


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

def reset_ancestor_bridges!
  base_and_ancestors.includes(:source_bridge).each do |pipeline|
    break unless pipeline.bridge_waiting?

    pipeline.source_bridge.pending!
  end
end

#retriedObject


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

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

#retry_failed(current_user) ⇒ Object

rubocop: disable CodeReuse/ServiceClass


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

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

#retryable?Boolean

Returns:

  • (Boolean)

543
544
545
# File 'app/models/ci/pipeline.rb', line 543

def retryable?
  retryable_builds.any?
end

#root_ancestorObject


887
888
889
890
891
892
893
894
# File 'app/models/ci/pipeline.rb', line 887

def root_ancestor
  return self unless child?

  Gitlab::Ci::PipelineObjectHierarchy
    .new(self.class.unscoped.where(id: id), options: { same_project: true })
    .base_and_ancestors(hierarchy_order: :desc)
    .first
end

#same_family_pipeline_idsObject


856
857
858
859
860
861
862
863
864
865
866
# File 'app/models/ci/pipeline.rb', line 856

def same_family_pipeline_ids
  if Feature.enabled?(:ci_root_ancestor_for_pipeline_family, project, default_enabled: false)
    ::Gitlab::Ci::PipelineObjectHierarchy.new(
      self.class.where(id: root_ancestor), options: { same_project: true }
    ).base_and_descendants.select(:id)
  else
    ::Gitlab::Ci::PipelineObjectHierarchy.new(
      base_and_ancestors(same_project: true), options: { same_project: true }
    ).base_and_descendants.select(:id)
  end
end

#self_and_descendantsObject

Without using `unscoped`, caller scope is also included into the query. Using `unscoped` here will be redundant after Rails 6.1


881
882
883
884
885
# File 'app/models/ci/pipeline.rb', line 881

def self_and_descendants
  ::Gitlab::Ci::PipelineObjectHierarchy
    .new(self.class.unscoped.where(id: id), options: { same_project: true })
    .base_and_descendants
end

#set_status(new_status) ⇒ Object


726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
# File 'app/models/ci/pipeline.rb', line 726

def set_status(new_status)
  retry_optimistic_lock(self) do
    case new_status
    when 'created' then nil
    when 'waiting_for_resource' then request_resource
    when 'preparing' then prepare
    when 'pending' then enqueue
    when 'running' then run
    when 'success' then succeed
    when 'failed' then drop
    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


527
528
529
# File 'app/models/ci/pipeline.rb', line 527

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

#source_refObject


1073
1074
1075
1076
1077
1078
1079
# File 'app/models/ci/pipeline.rb', line 1073

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

#source_ref_pathObject


1117
1118
1119
1120
1121
1122
1123
# File 'app/models/ci/pipeline.rb', line 1117

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


1081
1082
1083
# File 'app/models/ci/pipeline.rb', line 1081

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

#stages_countObject


428
429
430
# File 'app/models/ci/pipeline.rb', line 428

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

#stages_namesObject


436
437
438
439
# File 'app/models/ci/pipeline.rb', line 436

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

#stuck?Boolean

Returns:

  • (Boolean)

539
540
541
# File 'app/models/ci/pipeline.rb', line 539

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

#terraform_reportsObject


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

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

#test_report_summaryObject


954
955
956
957
958
# File 'app/models/ci/pipeline.rb', line 954

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

#test_reportsObject


960
961
962
963
964
965
966
# File 'app/models/ci/pipeline.rb', line 960

def test_reports
  Gitlab::Ci::Reports::TestReports.new.tap do |test_reports|
    latest_report_builds(Ci::JobArtifact.test_reports).preload(:project).find_each do |build|
      build.collect_test_reports!(test_reports)
    end
  end
end

#top_level_worktree_pathsObject


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

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

#total_sizeObject


432
433
434
# File 'app/models/ci/pipeline.rb', line 432

def total_size
  statuses.count(:id)
end

#triggered_by?(current_user) ⇒ Boolean

Returns:

  • (Boolean)

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

def triggered_by?(current_user)
  user == current_user
end

#triggered_pipelines_with_preloadsObject


466
467
468
# File 'app/models/ci/pipeline.rb', line 466

def triggered_pipelines_with_preloads
  triggered_pipelines.preload(:source_job)
end

#update_durationObject


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

def update_duration
  return unless started_at

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

#valid_commit_shaObject


475
476
477
478
479
# File 'app/models/ci/pipeline.rb', line 475

def valid_commit_sha
  if self.sha == Gitlab::Git::BLANK_SHA
    self.errors.add(:sha, " cant be 00000000 (branch removal)")
  end
end

#warning_messages(limit: nil) ⇒ Object


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

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