pipeline.rb 26.8 KB
Newer Older
1 2
# frozen_string_literal: true

3
module Ci
4
  class Pipeline < ApplicationRecord
5
    extend Gitlab::Ci::Model
6
    include HasStatus
7
    include Importable
8
    include AfterCommitQueue
9
    include Presentable
10
    include Gitlab::OptimisticLocking
11
    include Gitlab::Utils::StrongMemoize
Shinya Maeda's avatar
Shinya Maeda committed
12
    include AtomicInternalId
13
    include EnumWithNil
14
    include HasRef
15
    include ShaAttribute
16
    include FromUnion
17 18 19

    sha_attribute :source_sha
    sha_attribute :target_sha
Kamil Trzcinski's avatar
WIP  
Kamil Trzcinski committed
20

21
    belongs_to :project, inverse_of: :all_pipelines
22
    belongs_to :user
23
    belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline'
24
    belongs_to :pipeline_schedule, class_name: 'Ci::PipelineSchedule'
Shinya Maeda's avatar
Shinya Maeda committed
25
    belongs_to :merge_request, class_name: 'MergeRequest'
26
    belongs_to :external_pull_request
27

Shinya Maeda's avatar
Shinya Maeda committed
28
    has_internal_id :iid, scope: :project, presence: false, init: ->(s) do
29
      s&.project&.all_pipelines&.maximum(:iid) || s&.project&.all_pipelines&.count
30
    end
Shinya Maeda's avatar
Shinya Maeda committed
31

32
    has_many :stages, -> { order(position: :asc) }, inverse_of: :pipeline
33
    has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline
34
    has_many :processables, -> { processables },
35
             class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline
36
    has_many :builds, foreign_key: :commit_id, inverse_of: :pipeline
37
    has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent
Shinya Maeda's avatar
init  
Shinya Maeda committed
38
    has_many :variables, class_name: 'Ci::PipelineVariable'
39 40
    has_many :deployments, through: :builds
    has_many :environments, -> { distinct }, through: :deployments
Felipe Artur's avatar
Felipe Artur committed
41 42 43

    # Merge requests for which the current pipeline is running against
    # the merge request's latest commit.
44
    has_many :merge_requests_as_head_pipeline, foreign_key: "head_pipeline_id", class_name: 'MergeRequest'
45

46
    has_many :pending_builds, -> { pending }, foreign_key: :commit_id, class_name: 'Ci::Build'
Shinya Maeda's avatar
Shinya Maeda committed
47
    has_many :retryable_builds, -> { latest.failed_or_canceled.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build'
48
    has_many :cancelable_statuses, -> { cancelable }, foreign_key: :commit_id, class_name: 'CommitStatus'
49
    has_many :manual_actions, -> { latest.manual_actions.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build'
50
    has_many :scheduled_actions, -> { latest.scheduled_actions.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build'
51
    has_many :artifacts, -> { latest.with_artifacts_not_expired.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build'
52

53 54
    has_many :auto_canceled_pipelines, class_name: 'Ci::Pipeline', foreign_key: 'auto_canceled_by_id'
    has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id'
55

James Fargher's avatar
James Fargher committed
56 57
    has_one :chat_data, class_name: 'Ci::PipelineChatData'

58 59
    accepts_nested_attributes_for :variables, reject_if: :persisted?

Douwe Maan's avatar
Douwe Maan committed
60
    delegate :id, to: :project, prefix: true
61
    delegate :full_path, to: :project, prefix: true
Douwe Maan's avatar
Douwe Maan committed
62

Douwe Maan's avatar
Douwe Maan committed
63 64
    validates :sha, presence: { unless: :importing? }
    validates :ref, presence: { unless: :importing? }
65 66 67
    validates :merge_request, presence: { if: :merge_request_event? }
    validates :merge_request, absence: { unless: :merge_request_event? }
    validates :tag, inclusion: { in: [false], if: :merge_request_event? }
68 69 70 71 72

    validates :external_pull_request, presence: { if: :external_pull_request_event? }
    validates :external_pull_request, absence: { unless: :external_pull_request_event? }
    validates :tag, inclusion: { in: [false], if: :external_pull_request_event? }

Douwe Maan's avatar
Douwe Maan committed
73
    validates :status, presence: { unless: :importing? }
74
    validate :valid_commit_sha, unless: :importing?
Jasper Maes's avatar
Jasper Maes committed
75
    validates :source, exclusion: { in: %w(unknown), unless: :importing? }, on: :create
76

77
    after_create :keep_around_commits, unless: :importing?
Kamil Trzcinski's avatar
Kamil Trzcinski committed
78

79 80 81
    # We use `Ci::PipelineEnums.sources` here so that EE can more easily extend
    # this `Hash` with new values.
    enum_with_nil source: ::Ci::PipelineEnums.sources
82

83
    enum_with_nil config_source: ::Ci::PipelineEnums.config_sources
84

85 86 87
    # We use `Ci::PipelineEnums.failure_reasons` here so that EE can more easily
    # extend this `Hash` with new values.
    enum failure_reason: ::Ci::PipelineEnums.failure_reasons
88

89
    state_machine :status, initial: :created do
90
      event :enqueue do
Tiger's avatar
Tiger committed
91
        transition [:created, :preparing, :skipped, :scheduled] => :pending
92
        transition [:success, :failed, :canceled] => :running
93 94
      end

Tiger's avatar
Tiger committed
95 96 97 98
      event :prepare do
        transition any - [:preparing] => :preparing
      end

99
      event :run do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
100
        transition any - [:running] => :running
101 102
      end

103
      event :skip do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
104
        transition any - [:skipped] => :skipped
105 106 107
      end

      event :drop do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
108
        transition any - [:failed] => :failed
109 110
      end

111
      event :succeed do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
112
        transition any - [:success] => :success
113 114 115
      end

      event :cancel do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
116
        transition any - [:canceled] => :canceled
117 118
      end

119
      event :block do
120
        transition any - [:manual] => :manual
121 122
      end

123
      event :delay do
124 125 126
        transition any - [:scheduled] => :scheduled
      end

127 128 129 130
      # IMPORTANT
      # Do not add any operations to this state_machine
      # Create a separate worker for each new operation

Tiger's avatar
Tiger committed
131
      before_transition [:created, :preparing, :pending] => :running do |pipeline|
132
        pipeline.started_at = Time.now
133 134
      end

135
      before_transition any => [:success, :failed, :canceled] do |pipeline|
136
        pipeline.finished_at = Time.now
137 138 139
        pipeline.update_duration
      end

140 141 142 143
      before_transition any => [:manual] do |pipeline|
        pipeline.update_duration
      end

Lin Jen-Shin's avatar
Lin Jen-Shin committed
144
      before_transition canceled: any - [:canceled] do |pipeline|
Lin Jen-Shin's avatar
Lin Jen-Shin committed
145 146 147
        pipeline.auto_canceled_by = nil
      end

148 149 150 151 152 153
      before_transition any => :failed do |pipeline, transition|
        transition.args.first.try do |reason|
          pipeline.failure_reason = reason
        end
      end

Tiger's avatar
Tiger committed
154
      after_transition [:created, :preparing, :pending] => :running do |pipeline|
155
        pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
156 157 158
      end

      after_transition any => [:success] do |pipeline|
159
        pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
160 161
      end

Tiger's avatar
Tiger committed
162
      after_transition [:created, :preparing, :pending, :running] => :success do |pipeline|
163
        pipeline.run_after_commit { PipelineSuccessWorker.perform_async(pipeline.id) }
164
      end
165 166

      after_transition do |pipeline, transition|
167 168 169
        next if transition.loopback?

        pipeline.run_after_commit do
170
          PipelineHooksWorker.perform_async(pipeline.id)
171
          ExpirePipelineCacheWorker.perform_async(pipeline.id)
172
        end
173
      end
174

175 176 177 178 179 180 181 182 183 184
      after_transition any => ::Ci::Pipeline.completed_statuses do |pipeline|
        pipeline.run_after_commit do
          pipeline.all_merge_requests.each do |merge_request|
            next unless merge_request.auto_merge_enabled?

            AutoMergeProcessWorker.perform_async(merge_request.id)
          end
        end
      end

185
      after_transition any => [:success, :failed] do |pipeline|
186
        pipeline.run_after_commit do
187
          PipelineNotificationWorker.perform_async(pipeline.id)
188
        end
189
      end
190 191 192 193 194 195

      after_transition any => [:failed] do |pipeline|
        next unless pipeline.auto_devops_source?

        pipeline.run_after_commit { AutoDevops::DisableWorker.perform_async(pipeline.id) }
      end
196 197
    end

198
    scope :internal, -> { where(source: internal_sources) }
199
    scope :ci_sources, -> { where(config_source: ci_sources_values) }
200

Shinya Maeda's avatar
Shinya Maeda committed
201 202
    scope :sort_by_merge_request_pipelines, -> do
      sql = 'CASE ci_pipelines.source WHEN (?) THEN 0 ELSE 1 END, ci_pipelines.id DESC'
203
      query = ApplicationRecord.send(:sanitize_sql_array, [sql, sources[:merge_request_event]]) # rubocop:disable GitlabSecurity/PublicSend
Shinya Maeda's avatar
Shinya Maeda committed
204

Heinrich Lee Yu's avatar
Heinrich Lee Yu committed
205
      order(Arel.sql(query))
Shinya Maeda's avatar
Shinya Maeda committed
206 207
    end

208
    scope :for_user, -> (user) { where(user: user) }
209 210 211
    scope :for_sha, -> (sha) { where(sha: sha) }
    scope :for_source_sha, -> (source_sha) { where(source_sha: source_sha) }
    scope :for_sha_or_source_sha, -> (sha) { for_sha(sha).or(for_source_sha(sha)) }
212
    scope :created_after, -> (time) { where('ci_pipelines.created_at > ?', time) }
213

214
    scope :triggered_by_merge_request, -> (merge_request) do
215
      where(source: :merge_request_event, merge_request: merge_request)
216 217
    end

218 219
    scope :detached_merge_request_pipelines, -> (merge_request, sha) do
      triggered_by_merge_request(merge_request).for_sha(sha)
220 221
    end

222 223
    scope :merge_request_pipelines, -> (merge_request, source_sha) do
      triggered_by_merge_request(merge_request).for_source_sha(source_sha)
224 225
    end

226 227 228 229
    scope :triggered_for_branch, -> (ref) do
      where(source: branch_pipeline_sources).where(ref: ref, tag: false)
    end

Matija Čupić's avatar
Matija Čupić committed
230 231 232 233
    scope :with_reports, -> (reports_scope) do
      where('EXISTS (?)', ::Ci::Build.latest.with_reports(reports_scope).where('ci_pipelines.id=ci_builds.commit_id').select(1))
    end

234 235 236 237 238 239 240 241
    scope :without_interruptible_builds, -> do
      where('NOT EXISTS (?)',
        Ci::Build.where('ci_builds.commit_id = ci_pipelines.id')
                 .with_status(:running, :success, :failed)
                 .not_interruptible
      )
    end

242 243 244 245 246
    # 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.
247
    # sha - The commit SHA (or mutliple SHAs) to limit the list of pipelines to.
248
    # limit - This limits a backlog search, default to 100.
249
    def self.newest_first(ref: nil, sha: nil, limit: 100)
250
      relation = order(id: :desc)
251
      relation = relation.where(ref: ref) if ref
252
      relation = relation.where(sha: sha) if sha
253

254 255 256 257 258 259
      if limit
        ids = relation.limit(limit).select(:id)
        relation = relation.where(id: ids)
      end

      relation
260
    end
261

262
    def self.latest_status(ref = nil)
263
      newest_first(ref: ref).pluck(:status).first
264 265
    end

266
    def self.latest_successful_for_ref(ref)
267
      newest_first(ref: ref).success.take
268 269
    end

270 271 272 273
    def self.latest_successful_for_sha(sha)
      newest_first(sha: sha).success.take
    end

274
    def self.latest_successful_for_refs(refs)
275
      relation = newest_first(ref: refs).success
276 277

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

282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316
    # Returns a Hash containing the latest pipeline status for every given
    # commit.
    #
    # The keys of this Hash are the commit SHAs, the values the statuses.
    #
    # commits - The list of commit SHAs to get the status for.
    # ref - The ref to scope the data to (e.g. "master"). If the ref is not
    #       given we simply get the latest status for the commits, regardless
    #       of what refs their pipelines belong to.
    def self.latest_status_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 = select(:sha, :status)
        .where(sha: commits)
        .where(p2[:id].eq(nil))
        .joins(join.join_sources)

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

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

317 318 319 320 321
    def self.latest_for_shas(shas)
      max_id_per_sha = for_sha(shas).group(:sha).select("max(id)")
      where(id: max_id_per_sha)
    end

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

326 327 328 329
    def self.truncate_sha(sha)
      sha[0...8]
    end

330
    def self.total_duration
Lin Jen-Shin's avatar
Lin Jen-Shin committed
331
      where.not(duration: nil).sum(:duration)
332 333
    end

334 335 336 337
    def self.internal_sources
      sources.reject { |source| source == "external" }.values
    end

338 339
    def self.branch_pipeline_sources
      @branch_pipeline_sources ||= sources.reject { |source| source == 'merge_request_event' }.values
340 341
    end

342 343 344 345
    def self.ci_sources_values
      config_sources.values_at(:repository_source, :auto_devops_source, :unknown_source)
    end

Matija Čupić's avatar
Matija Čupić committed
346 347 348 349
    def self.bridgeable_statuses
      ::Ci::Pipeline::AVAILABLE_STATUSES - %w[created preparing pending]
    end

350 351
    def stages_count
      statuses.select(:stage).distinct.count
Kamil Trzcinski's avatar
Kamil Trzcinski committed
352 353
    end

354 355 356 357
    def total_size
      statuses.count(:id)
    end

358
    def stages_names
359 360
      statuses.order(:stage_idx).distinct
        .pluck(:stage, :stage_idx).map(&:first)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
361 362
    end

363
    def legacy_stage(name)
364
      stage = Ci::LegacyStage.new(self, name: name)
365 366 367
      stage unless stage.statuses_count.zero?
    end

368
    def ref_exists?
369
      project.repository.ref_exists?(git_ref)
370 371
    rescue Gitlab::Git::Repository::NoRepository
      false
372 373
    end

374
    ##
375 376
    # TODO We do not completely switch to persisted stages because of
    # race conditions with setting statuses gitlab-ce#23257.
377
    #
378 379 380
    def ordered_stages
      return legacy_stages unless complete?

381
      if Feature.enabled?('ci_pipeline_persisted_stages', default_enabled: true)
382 383 384 385
        stages
      else
        legacy_stages
      end
386 387
    end

388
    def legacy_stages
389 390
      # TODO, this needs refactoring, see gitlab-ce#26481.

391 392
      stages_query = statuses
        .group('stage').select(:stage).order('max(stage_idx)')
393

Kamil Trzcinski's avatar
Kamil Trzcinski committed
394 395
      status_sql = statuses.latest.where('stage=sg.stage').status_sql

396
      warnings_sql = statuses.latest.select('COUNT(*)')
397
        .where('stage=sg.stage').failed_but_allowed.to_sql
398

399 400
      stages_with_statuses = CommitStatus.from(stages_query, :sg)
        .pluck('sg.stage', status_sql, "(#{warnings_sql})")
Kamil Trzcinski's avatar
Kamil Trzcinski committed
401 402

      stages_with_statuses.map do |stage|
403
        Ci::LegacyStage.new(self, Hash[%i[name status warnings].zip(stage)])
Kamil Trzcinski's avatar
Kamil Trzcinski committed
404 405 406
      end
    end

407
    def valid_commit_sha
408
      if self.sha == Gitlab::Git::BLANK_SHA
409 410 411 412 413
        self.errors.add(:sha, " cant be 00000000 (branch removal)")
      end
    end

    def git_author_name
414 415 416
      strong_memoize(:git_author_name) do
        commit.try(:author_name)
      end
417 418 419
    end

    def git_author_email
420 421 422
      strong_memoize(:git_author_email) do
        commit.try(:author_email)
      end
423 424 425
    end

    def git_commit_message
426 427 428
      strong_memoize(:git_commit_message) do
        commit.try(:message)
      end
429 430
    end

431
    def git_commit_title
432 433 434
      strong_memoize(:git_commit_title) do
        commit.try(:title)
      end
435 436
    end

437
    def git_commit_full_title
438 439 440
      strong_memoize(:git_commit_full_title) do
        commit.try(:full_title)
      end
441 442 443
    end

    def git_commit_description
444 445 446
      strong_memoize(:git_commit_description) do
        commit.try(:description)
      end
447 448
    end

449
    def short_sha
450
      Ci::Pipeline.truncate_sha(sha)
451 452
    end

453 454 455 456
    # NOTE: This is loaded lazily and will never be nil, even if the commit
    # cannot be found.
    #
    # Use constructs like: `pipeline.commit.present?`
457
    def commit
458
      @commit ||= Commit.lazy(project, sha)
459 460
    end

461
    def stuck?
462
      pending_builds.any?(&:stuck?)
463 464
    end

465
    def retryable?
466
      retryable_builds.any?
467 468
    end

469
    def cancelable?
470
      cancelable_statuses.any?
471 472
    end

Lin Jen-Shin's avatar
Lin Jen-Shin committed
473 474 475 476
    def auto_canceled?
      canceled? && auto_canceled_by_id?
    end

Sean Carroll's avatar
Sean Carroll committed
477 478
    def cancel_running(retries: nil)
      retry_optimistic_lock(cancelable_statuses, retries) do |cancelable|
Lin Jen-Shin's avatar
Lin Jen-Shin committed
479 480 481
        cancelable.find_each do |job|
          yield(job) if block_given?
          job.cancel
482
        end
Lin Jen-Shin's avatar
Lin Jen-Shin committed
483
      end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
484 485
    end

Sean Carroll's avatar
Sean Carroll committed
486
    def auto_cancel_running(pipeline, retries: nil)
487 488
      update(auto_canceled_by: pipeline)

Sean Carroll's avatar
Sean Carroll committed
489
      cancel_running(retries: retries) do |job|
490
        job.auto_canceled_by = pipeline
491
      end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
492 493
    end

494
    # rubocop: disable CodeReuse/ServiceClass
495
    def retry_failed(current_user)
496 497
      Ci::RetryPipelineService.new(project, current_user)
        .execute(self)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
498
    end
499
    # rubocop: enable CodeReuse/ServiceClass
Kamil Trzcinski's avatar
Kamil Trzcinski committed
500

501
    def mark_as_processable_after_stage(stage_idx)
502
      builds.skipped.after_stage(stage_idx).find_each(&:process)
503 504
    end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
505
    def latest?
506
      return false unless git_ref && commit.present?
507

508
      project.commit(git_ref) == commit
Kamil Trzcinski's avatar
Kamil Trzcinski committed
509 510
    end

511 512
    def retried
      @retried ||= (statuses.order(id: :desc) - statuses.latest)
513 514 515
    end

    def coverage
516
      coverage_array = statuses.latest.map(&:coverage).compact
517 518
      if coverage_array.size >= 1
        '%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
519 520 521
      end
    end

522
    def stage_seeds
523 524
      return [] unless config_processor

525
      strong_memoize(:stage_seeds) do
526 527 528
        seeds = config_processor.stages_attributes.inject([]) do |previous_stages, attributes|
          seed = Gitlab::Ci::Pipeline::Seed::Stage.new(self, attributes, previous_stages)
          previous_stages + [seed]
529 530 531 532
        end

        seeds.select(&:included?)
      end
533 534
    end

535
    def seeds_size
536
      stage_seeds.sum(&:size)
537 538
    end

539
    def has_kubernetes_active?
540
      project.deployment_platform&.active?
541 542
    end

Connor Shea's avatar
Connor Shea committed
543
    def has_warnings?
544 545 546 547 548
      number_of_warnings.positive?
    end

    def number_of_warnings
      BatchLoader.for(id).batch(default_value: 0) do |pipeline_ids, loader|
549
        ::Ci::Build.where(commit_id: pipeline_ids)
550 551 552 553 554 555
          .latest
          .failed_but_allowed
          .group(:commit_id)
          .count
          .each { |id, amount| loader.call(id, amount) }
      end
556 557
    end

558
    def set_config_source
559 560 561 562 563
      if ci_yaml_from_repo
        self.config_source = :repository_source
      elsif implied_ci_yaml_file
        self.config_source = :auto_devops_source
      end
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
564 565
    end

566 567 568
    ##
    # TODO, setting yaml_errors should be moved to the pipeline creation chain.
    #
569
    def config_processor
570
      return unless ci_yaml_file
571 572 573
      return @config_processor if defined?(@config_processor)

      @config_processor ||= begin
574
        ::Gitlab::Ci::YamlProcessor.new(ci_yaml_file, { project: project, sha: sha, user: user })
575
      rescue Gitlab::Ci::YamlProcessor::ValidationError => e
576
        self.yaml_errors = e.message
577 578
        nil
      rescue
579
        self.yaml_errors = 'Undefined error'
580 581
        nil
      end
582 583
    end

584
    def ci_yaml_file_path
585 586
      return unless repository_source? || unknown_source?

587
      if project.ci_config_path.blank?
588 589
        '.gitlab-ci.yml'
      else
590
        project.ci_config_path
591 592 593
      end
    end

594
    def ci_yaml_file
595 596
      return @ci_yaml_file if defined?(@ci_yaml_file)

Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
597
      @ci_yaml_file =
598
        if auto_devops_source?
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
599
          implied_ci_yaml_file
600 601
        else
          ci_yaml_from_repo
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
602
        end
603 604 605 606 607 608

      if @ci_yaml_file
        @ci_yaml_file
      else
        self.yaml_errors = "Failed to load CI/CD config file for #{sha}"
        nil
609
      end
610 611
    end

612 613 614 615
    def has_yaml_errors?
      yaml_errors.present?
    end

James Lopez's avatar
James Lopez committed
616 617 618 619 620 621 622 623 624 625 626 627 628
    # 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::RelationFactory+ class.
    def notes=(notes)
      notes.each do |note|
        note[:id] = nil
        note[:commit_id] = sha
        note[:noteable_id] = self['id']
        note.save!
      end
    end

629
    def notes
630
      project.notes.for_commit_id(sha)
631 632
    end

633
    # rubocop: disable CodeReuse/ServiceClass
634 635
    def process!(trigger_build_ids = nil)
      Ci::ProcessPipelineService.new(project, user).execute(self, trigger_build_ids)
636
    end
637
    # rubocop: enable CodeReuse/ServiceClass
638

639
    def update_status
640
      retry_optimistic_lock(self) do
641 642
        case latest_builds_status.to_s
        when 'created' then nil
Tiger's avatar
Tiger committed
643
        when 'preparing' then prepare
644 645 646 647 648 649
        when 'pending' then enqueue
        when 'running' then run
        when 'success' then succeed
        when 'failed' then drop
        when 'canceled' then cancel
        when 'skipped' then skip
650
        when 'manual' then block
651
        when 'scheduled' then delay
652 653 654
        else
          raise HasStatus::UnknownStatusError,
                "Unknown status `#{latest_builds_status}`"
655
        end
656
      end
657 658
    end

659
    def protected_ref?
660
      strong_memoize(:protected_ref) { project.protected_for?(git_ref) }
661 662 663
    end

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

667 668
    def persisted_variables
      Gitlab::Ci::Variables::Collection.new.tap do |variables|
669 670 671 672
        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))
673 674 675
      end
    end

676
    def predefined_variables
677 678 679 680 681 682 683
      Gitlab::Ci::Variables::Collection.new.tap do |variables|
        variables.append(key: 'CI_PIPELINE_IID', value: iid.to_s)
        variables.append(key: 'CI_CONFIG_PATH', value: ci_yaml_file_path)
        variables.append(key: 'CI_PIPELINE_SOURCE', value: source.to_s)
        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)
684
        variables.append(key: 'CI_COMMIT_REF_PROTECTED', value: (!!protected_ref?).to_s)
685

686
        if merge_request_event? && merge_request
687
          variables.append(key: 'CI_MERGE_REQUEST_EVENT_TYPE', value: merge_request_event_type.to_s)
688 689
          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)
690 691
          variables.concat(merge_request.predefined_variables)
        end
692 693 694 695

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

699 700 701 702 703 704 705
    def queued_duration
      return unless started_at

      seconds = (started_at - created_at).to_i
      seconds unless seconds.zero?
    end

706
    def update_duration
707 708
      return unless started_at

709
      self.duration = Gitlab::Ci::Pipeline::Duration.from_pipeline(self)
710 711 712
    end

    def execute_hooks
713 714 715
      data = pipeline_data
      project.execute_hooks(data, :pipeline_hooks)
      project.execute_services(data, :pipeline_hooks)
716 717
    end

718 719
    # All the merge requests for which the current pipeline runs/ran against
    def all_merge_requests
Shinya Maeda's avatar
Shinya Maeda committed
720
      @all_merge_requests ||=
721
        if merge_request_event?
722
          MergeRequest.where(id: merge_request_id)
Shinya Maeda's avatar
Shinya Maeda committed
723
        else
724
          MergeRequest.where(source_project_id: project_id, source_branch: ref)
Shinya Maeda's avatar
Shinya Maeda committed
725
        end
726 727
    end

728
    def detailed_status(current_user)
729 730 731
      Gitlab::Ci::Status::Pipeline::Factory
        .new(self, current_user)
        .fabricate!
732 733
    end

734
    def latest_builds_with_artifacts
735 736 737
      # 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.
738
      @latest_builds_with_artifacts ||= builds.latest.with_artifacts_not_expired.to_a
739 740
    end

Matija Čupić's avatar
Matija Čupić committed
741 742
    def has_reports?(reports_scope)
      complete? && builds.latest.with_reports(reports_scope).exists?
743 744 745 746
    end

    def test_reports
      Gitlab::Ci::Reports::TestReports.new.tap do |test_reports|
Matija Čupić's avatar
Matija Čupić committed
747
        builds.latest.with_reports(Ci::JobArtifact.test_reports).each do |build|
748 749 750 751 752
          build.collect_test_reports!(test_reports)
        end
      end
    end

753 754 755 756 757 758
    def branch_updated?
      strong_memoize(:branch_updated) do
        push_details.branch_updated?
      end
    end

759 760 761 762 763
    # Returns the modified paths.
    #
    # The returned value is
    # * Array: List of modified paths that should be evaluated
    # * nil: Modified path can not be evaluated
764
    def modified_paths
765
      strong_memoize(:modified_paths) do
766
        if merge_request_event?
767 768 769 770
          merge_request.modified_paths
        elsif branch_updated?
          push_details.modified_paths
        end
771 772 773
      end
    end

774 775 776 777
    def default_branch?
      ref == project.default_branch
    end

778
    def triggered_by_merge_request?
779
      merge_request_event? && merge_request_id.present?
780 781 782 783 784 785
    end

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

786 787 788 789
    def legacy_detached_merge_request_pipeline?
      detached_merge_request_pipeline? && !merge_request_ref?
    end

790 791 792 793
    def merge_request_pipeline?
      triggered_by_merge_request? && target_sha.present?
    end

794 795 796 797
    def merge_train_pipeline?
      merge_request_pipeline? && merge_train_ref?
    end

798 799 800 801
    def merge_request_ref?
      MergeRequest.merge_request_ref?(ref)
    end

802 803 804 805
    def merge_train_ref?
      MergeRequest.merge_train_ref?(ref)
    end

806 807 808 809
    def matches_sha_or_source_sha?(sha)
      self.sha == sha || self.source_sha == sha
    end

810 811 812 813
    def triggered_by?(current_user)
      user == current_user
    end

814 815 816 817 818 819 820 821 822 823 824 825
    def source_ref
      if triggered_by_merge_request?
        merge_request.source_branch
      else
        ref
      end
    end

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

826 827 828 829
    def find_stage_by_name!(name)
      stages.find_by!(name: name)
    end

830 831 832 833
    def error_messages
      errors ? errors.full_messages.to_sentence : ""
    end

834 835 836 837 838 839 840 841 842 843 844 845 846 847
    def merge_request_event_type
      return unless merge_request_event?

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

848 849
    private

850
    def ci_yaml_from_repo
851 852
      return unless project
      return unless sha
853
      return unless ci_yaml_file_path
854

855
      project.repository.gitlab_ci_yml_for(sha, ci_yaml_file_path)
856
    rescue GRPC::NotFound, GRPC::Internal
857 858 859
      nil
    end

Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
860
    def implied_ci_yaml_file
861 862
      return unless project

Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
863 864 865 866 867
      if project.auto_devops_enabled?
        Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content
      end
    end

868
    def pipeline_data
869
      Gitlab::DataBuilder::Pipeline.build(self)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
870
    end
871

872 873
    def push_details
      strong_memoize(:push_details) do
874
        Gitlab::Git::Push.new(project, before_sha, sha, git_ref)
875 876 877
      end
    end

878
    def git_ref
879
      strong_memoize(:git_ref) do
880 881 882 883 884 885 886 887 888 889 890
        if merge_request_event?
          ##
          # In the future, we're going to change this ref to
          # merge request's merged reference, such as "refs/merge-requests/:iid/merge".
          # In order to do that, we have to update GitLab-Runner's source pulling
          # logic.
          # See https://gitlab.com/gitlab-org/gitlab-runner/merge_requests/1092
          Gitlab::Git::BRANCH_REF_PREFIX + ref.to_s
        else
          super
        end
891 892 893
      end
    end

894
    def latest_builds_status
895 896 897
      return 'failed' unless yaml_errors.blank?

      statuses.latest.status || 'skipped'
Kamil Trzcinski's avatar
Kamil Trzcinski committed
898
    end
899 900

    def keep_around_commits
901
      return unless project
902

903
      project.repository.keep_around(self.sha, self.before_sha)
904
    end
905 906
  end
end