Read about our upcoming Code of Conduct on this issue

This instance was upgraded to Heptapod 0.28.0 today

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

Douwe Maan's avatar
Douwe Maan committed
3
module Ci
4
  class Pipeline < ActiveRecord::Base
5
    extend Gitlab::Ci::Model
6
    include HasStatus
7
    include Importable
8
    include AfterCommitQueue
Rydkin Maxim's avatar
Rydkin Maxim committed
9
    include Presentable
10
    include Gitlab::OptimisticLocking
11
    include Gitlab::Utils::StrongMemoize
Shinya Maeda's avatar
Shinya Maeda committed
12
    include AtomicInternalId
Jan Provaznik's avatar
Jan Provaznik committed
13
    include EnumWithNil
Kamil Trzcinski's avatar
WIP    
Kamil Trzcinski committed
14

15
    belongs_to :project, inverse_of: :all_pipelines
16
    belongs_to :user
17
    belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline'
18
    belongs_to :pipeline_schedule, class_name: 'Ci::PipelineSchedule'
Shinya Maeda's avatar
Shinya Maeda committed
19
    belongs_to :merge_request, class_name: 'MergeRequest'
20

Shinya Maeda's avatar
Shinya Maeda committed
21
    has_internal_id :iid, scope: :project, presence: false, init: ->(s) do
22
      s&.project&.all_pipelines&.maximum(:iid) || s&.project&.all_pipelines&.count
23
    end
Shinya Maeda's avatar
Shinya Maeda committed
24

25
    has_many :stages, -> { order(position: :asc) }, inverse_of: :pipeline
26
    has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id, inverse_of: :pipeline
27
    has_many :builds, foreign_key: :commit_id, inverse_of: :pipeline
28
    has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent
Shinya Maeda's avatar
init    
Shinya Maeda committed
29
    has_many :variables, class_name: 'Ci::PipelineVariable'
30
31
    has_many :deployments, through: :builds
    has_many :environments, -> { distinct }, through: :deployments
Felipe Artur's avatar
Felipe Artur committed
32
33
34

    # Merge requests for which the current pipeline is running against
    # the merge request's latest commit.
35
    has_many :merge_requests, foreign_key: "head_pipeline_id"
Douwe Maan's avatar
Douwe Maan committed
36

37
    has_many :pending_builds, -> { pending }, foreign_key: :commit_id, class_name: 'Ci::Build'
Shinya Maeda's avatar
Shinya Maeda committed
38
    has_many :retryable_builds, -> { latest.failed_or_canceled.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build'
39
    has_many :cancelable_statuses, -> { cancelable }, foreign_key: :commit_id, class_name: 'CommitStatus'
40
    has_many :manual_actions, -> { latest.manual_actions.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build'
41
    has_many :scheduled_actions, -> { latest.scheduled_actions.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build'
42
    has_many :artifacts, -> { latest.with_artifacts_not_expired.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build'
43

44
45
    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'
46

47
48
    accepts_nested_attributes_for :variables, reject_if: :persisted?

Douwe Maan's avatar
Douwe Maan committed
49
    delegate :id, to: :project, prefix: true
50
    delegate :full_path, to: :project, prefix: true
Douwe Maan's avatar
Douwe Maan committed
51

Douwe Maan's avatar
Douwe Maan committed
52
53
    validates :sha, presence: { unless: :importing? }
    validates :ref, presence: { unless: :importing? }
Shinya Maeda's avatar
Shinya Maeda committed
54
55
56
    validates :merge_request, presence: { if: :merge_request? }
    validates :merge_request, absence: { unless: :merge_request? }
    validates :tag, inclusion: { in: [false], if: :merge_request? }
Douwe Maan's avatar
Douwe Maan committed
57
    validates :status, presence: { unless: :importing? }
58
    validate :valid_commit_sha, unless: :importing?
Douwe Maan's avatar
Douwe Maan committed
59

60
61
62
63
64
    # Replace validator below with
    # `validates :source, presence: { unless: :importing? }, on: :create`
    # when removing Gitlab.rails5? code.
    validate :valid_source, unless: :importing?, on: :create

65
    after_create :keep_around_commits, unless: :importing?
Kamil Trzcinski's avatar
Kamil Trzcinski committed
66

67
68
69
    # 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
70

Jan Provaznik's avatar
Jan Provaznik committed
71
    enum_with_nil config_source: {
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
72
73
74
      unknown_source: nil,
      repository_source: 1,
      auto_devops_source: 2
75
76
    }

77
78
79
    # 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
80

81
    state_machine :status, initial: :created do
82
      event :enqueue do
83
        transition [:created, :skipped, :scheduled] => :pending
84
        transition [:success, :failed, :canceled] => :running
85
86
87
      end

      event :run do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
88
        transition any - [:running] => :running
89
90
      end

91
      event :skip do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
92
        transition any - [:skipped] => :skipped
93
94
95
      end

      event :drop do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
96
        transition any - [:failed] => :failed
97
98
      end

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

      event :cancel do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
104
        transition any - [:canceled] => :canceled
105
106
      end

107
      event :block do
108
        transition any - [:manual] => :manual
109
110
      end

111
      event :delay do
112
113
114
        transition any - [:scheduled] => :scheduled
      end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
115
116
117
118
      # IMPORTANT
      # Do not add any operations to this state_machine
      # Create a separate worker for each new operation

119
      before_transition [:created, :pending] => :running do |pipeline|
120
        pipeline.started_at = Time.now
121
122
      end

123
      before_transition any => [:success, :failed, :canceled] do |pipeline|
124
        pipeline.finished_at = Time.now
125
126
127
        pipeline.update_duration
      end

128
129
130
131
      before_transition any => [:manual] do |pipeline|
        pipeline.update_duration
      end

Lin Jen-Shin's avatar
Lin Jen-Shin committed
132
      before_transition canceled: any - [:canceled] do |pipeline|
Lin Jen-Shin's avatar
Lin Jen-Shin committed
133
134
135
        pipeline.auto_canceled_by = nil
      end

136
137
138
139
140
141
      before_transition any => :failed do |pipeline, transition|
        transition.args.first.try do |reason|
          pipeline.failure_reason = reason
        end
      end

142
      after_transition [:created, :pending] => :running do |pipeline|
143
        pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
144
145
146
      end

      after_transition any => [:success] do |pipeline|
147
        pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
148
149
      end

150
      after_transition [:created, :pending, :running] => :success do |pipeline|
151
        pipeline.run_after_commit { PipelineSuccessWorker.perform_async(pipeline.id) }
152
      end
153
154

      after_transition do |pipeline, transition|
155
156
157
        next if transition.loopback?

        pipeline.run_after_commit do
158
          PipelineHooksWorker.perform_async(pipeline.id)
159
          ExpirePipelineCacheWorker.perform_async(pipeline.id)
160
        end
161
      end
162

163
      after_transition any => [:success, :failed] do |pipeline|
164
        pipeline.run_after_commit do
165
          PipelineNotificationWorker.perform_async(pipeline.id)
166
        end
167
      end
168
169
170
171
172
173

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

        pipeline.run_after_commit { AutoDevops::DisableWorker.perform_async(pipeline.id) }
      end
174
175
    end

176
    scope :internal, -> { where(source: internal_sources) }
177
    scope :ci_sources, -> { where(config_source: ci_sources_values) }
178

Shinya Maeda's avatar
Shinya Maeda committed
179
180
181
182
183
184
185
    scope :sort_by_merge_request_pipelines, -> do
      sql = 'CASE ci_pipelines.source WHEN (?) THEN 0 ELSE 1 END, ci_pipelines.id DESC'
      query = ActiveRecord::Base.send(:sanitize_sql_array, [sql, sources[:merge_request]]) # rubocop:disable GitlabSecurity/PublicSend

      order(query)
    end

186
187
    scope :for_user, -> (user) { where(user: user) }

188
189
190
191
192
    # 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.
193
194
    # limit - This limits a backlog search, default to 100.
    def self.newest_first(ref: nil, limit: 100)
195
      relation = order(id: :desc)
196
      relation = relation.where(ref: ref) if ref
197

198
199
200
201
202
203
204
205
      if limit
        ids = relation.limit(limit).select(:id)
        # MySQL does not support limit in subquery
        ids = ids.pluck(:id) if Gitlab::Database.mysql?
        relation = relation.where(id: ids)
      end

      relation
206
    end
207

208
    def self.latest_status(ref = nil)
209
      newest_first(ref: ref).pluck(:status).first
210
211
    end

212
    def self.latest_successful_for(ref)
213
      newest_first(ref: ref).success.take
214
215
    end

216
    def self.latest_successful_for_refs(refs)
217
      relation = newest_first(ref: refs).success
218
219

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

224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
    # 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

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

Douwe Maan's avatar
Douwe Maan committed
263
264
265
266
    def self.truncate_sha(sha)
      sha[0...8]
    end

267
    def self.total_duration
Lin Jen-Shin's avatar
Lin Jen-Shin committed
268
      where.not(duration: nil).sum(:duration)
269
270
    end

271
272
273
274
    def self.internal_sources
      sources.reject { |source| source == "external" }.values
    end

275
276
277
278
    def self.ci_sources_values
      config_sources.values_at(:repository_source, :auto_devops_source, :unknown_source)
    end

279
280
    def stages_count
      statuses.select(:stage).distinct.count
Kamil Trzcinski's avatar
Kamil Trzcinski committed
281
282
    end

283
284
285
286
    def total_size
      statuses.count(:id)
    end

287
    def stages_names
288
289
      statuses.order(:stage_idx).distinct
        .pluck(:stage, :stage_idx).map(&:first)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
290
291
    end

292
    def legacy_stage(name)
293
      stage = Ci::LegacyStage.new(self, name: name)
294
295
296
      stage unless stage.statuses_count.zero?
    end

297
    def ref_exists?
298
      project.repository.ref_exists?(git_ref)
299
300
    rescue Gitlab::Git::Repository::NoRepository
      false
301
302
    end

303
    ##
304
305
    # TODO We do not completely switch to persisted stages because of
    # race conditions with setting statuses gitlab-ce#23257.
306
    #
307
308
309
310
311
312
313
314
    def ordered_stages
      return legacy_stages unless complete?

      if Feature.enabled?('ci_pipeline_persisted_stages')
        stages
      else
        legacy_stages
      end
315
316
    end

317
    def legacy_stages
318
319
      # TODO, this needs refactoring, see gitlab-ce#26481.

Douwe Maan's avatar
Douwe Maan committed
320
321
      stages_query = statuses
        .group('stage').select(:stage).order('max(stage_idx)')
322

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

325
      warnings_sql = statuses.latest.select('COUNT(*)')
Douwe Maan's avatar
Douwe Maan committed
326
        .where('stage=sg.stage').failed_but_allowed.to_sql
327

Douwe Maan's avatar
Douwe Maan committed
328
329
      stages_with_statuses = CommitStatus.from(stages_query, :sg)
        .pluck('sg.stage', status_sql, "(#{warnings_sql})")
Kamil Trzcinski's avatar
Kamil Trzcinski committed
330
331

      stages_with_statuses.map do |stage|
332
        Ci::LegacyStage.new(self, Hash[%i[name status warnings].zip(stage)])
Kamil Trzcinski's avatar
Kamil Trzcinski committed
333
334
335
      end
    end

Douwe Maan's avatar
Douwe Maan committed
336
    def valid_commit_sha
337
      if self.sha == Gitlab::Git::BLANK_SHA
Douwe Maan's avatar
Douwe Maan committed
338
339
340
341
342
        self.errors.add(:sha, " cant be 00000000 (branch removal)")
      end
    end

    def git_author_name
343
344
345
      strong_memoize(:git_author_name) do
        commit.try(:author_name)
      end
Douwe Maan's avatar
Douwe Maan committed
346
347
348
    end

    def git_author_email
349
350
351
      strong_memoize(:git_author_email) do
        commit.try(:author_email)
      end
Douwe Maan's avatar
Douwe Maan committed
352
353
354
    end

    def git_commit_message
355
356
357
      strong_memoize(:git_commit_message) do
        commit.try(:message)
      end
Douwe Maan's avatar
Douwe Maan committed
358
359
    end

360
    def git_commit_title
361
362
363
      strong_memoize(:git_commit_title) do
        commit.try(:title)
      end
364
365
    end

366
    def git_commit_full_title
367
368
369
      strong_memoize(:git_commit_full_title) do
        commit.try(:full_title)
      end
370
371
372
    end

    def git_commit_description
373
374
375
      strong_memoize(:git_commit_description) do
        commit.try(:description)
      end
376
377
    end

Douwe Maan's avatar
Douwe Maan committed
378
    def short_sha
379
      Ci::Pipeline.truncate_sha(sha)
Douwe Maan's avatar
Douwe Maan committed
380
381
    end

382
383
384
385
    # NOTE: This is loaded lazily and will never be nil, even if the commit
    # cannot be found.
    #
    # Use constructs like: `pipeline.commit.present?`
386
    def commit
387
      @commit ||= Commit.lazy(project, sha)
Douwe Maan's avatar
Douwe Maan committed
388
389
    end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
390
    def branch?
Shinya Maeda's avatar
Shinya Maeda committed
391
      !tag? && !merge_request?
Kamil Trzcinski's avatar
Kamil Trzcinski committed
392
393
    end

394
    def stuck?
395
      pending_builds.any?(&:stuck?)
396
397
    end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
398
    def retryable?
399
      retryable_builds.any?
Kamil Trzcinski's avatar
Kamil Trzcinski committed
400
401
    end

402
    def cancelable?
403
      cancelable_statuses.any?
404
405
    end

Lin Jen-Shin's avatar
Lin Jen-Shin committed
406
407
408
409
    def auto_canceled?
      canceled? && auto_canceled_by_id?
    end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
410
    def cancel_running
411
      retry_optimistic_lock(cancelable_statuses) do |cancelable|
Lin Jen-Shin's avatar
Lin Jen-Shin committed
412
413
414
        cancelable.find_each do |job|
          yield(job) if block_given?
          job.cancel
415
        end
Lin Jen-Shin's avatar
Lin Jen-Shin committed
416
      end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
417
418
    end

419
420
421
422
423
    def auto_cancel_running(pipeline)
      update(auto_canceled_by: pipeline)

      cancel_running do |job|
        job.auto_canceled_by = pipeline
424
      end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
425
426
    end

427
    # rubocop: disable CodeReuse/ServiceClass
428
    def retry_failed(current_user)
Douwe Maan's avatar
Douwe Maan committed
429
430
      Ci::RetryPipelineService.new(project, current_user)
        .execute(self)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
431
    end
432
    # rubocop: enable CodeReuse/ServiceClass
Kamil Trzcinski's avatar
Kamil Trzcinski committed
433

434
    def mark_as_processable_after_stage(stage_idx)
435
      builds.skipped.after_stage(stage_idx).find_each(&:process)
436
437
    end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
438
    def latest?
439
      return false unless ref && commit.present?
440

441
      project.commit(ref) == commit
Kamil Trzcinski's avatar
Kamil Trzcinski committed
442
443
    end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
444
445
    def retried
      @retried ||= (statuses.order(id: :desc) - statuses.latest)
Douwe Maan's avatar
Douwe Maan committed
446
447
448
    end

    def coverage
449
      coverage_array = statuses.latest.map(&:coverage).compact
450
451
      if coverage_array.size >= 1
        '%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
Douwe Maan's avatar
Douwe Maan committed
452
453
454
      end
    end

455
    def stage_seeds
456
457
      return [] unless config_processor

458
      strong_memoize(:stage_seeds) do
459
        seeds = config_processor.stages_attributes.map do |attributes|
460
461
462
463
464
          Gitlab::Ci::Pipeline::Seed::Stage.new(self, attributes)
        end

        seeds.select(&:included?)
      end
465
466
    end

467
    def seeds_size
468
      stage_seeds.sum(&:size)
469
470
    end

471
    def has_kubernetes_active?
472
      project.deployment_platform&.active?
473
474
    end

Connor Shea's avatar
Connor Shea committed
475
    def has_warnings?
476
477
478
479
480
      number_of_warnings.positive?
    end

    def number_of_warnings
      BatchLoader.for(id).batch(default_value: 0) do |pipeline_ids, loader|
481
        ::Ci::Build.where(commit_id: pipeline_ids)
482
483
484
485
486
487
          .latest
          .failed_but_allowed
          .group(:commit_id)
          .count
          .each { |id, amount| loader.call(id, amount) }
      end
488
489
    end

490
    def set_config_source
491
492
493
494
495
      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
496
497
    end

498
499
500
    ##
    # TODO, setting yaml_errors should be moved to the pipeline creation chain.
    #
Douwe Maan's avatar
Douwe Maan committed
501
    def config_processor
502
      return unless ci_yaml_file
503
504
505
      return @config_processor if defined?(@config_processor)

      @config_processor ||= begin
506
        ::Gitlab::Ci::YamlProcessor.new(ci_yaml_file, { project: project, sha: sha })
507
      rescue Gitlab::Ci::YamlProcessor::ValidationError => e
508
        self.yaml_errors = e.message
509
510
        nil
      rescue
511
        self.yaml_errors = 'Undefined error'
512
513
        nil
      end
Douwe Maan's avatar
Douwe Maan committed
514
515
    end

516
    def ci_yaml_file_path
517
518
      return unless repository_source? || unknown_source?

519
      if project.ci_config_path.blank?
520
521
        '.gitlab-ci.yml'
      else
522
        project.ci_config_path
523
524
525
      end
    end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
526
    def ci_yaml_file
527
528
      return @ci_yaml_file if defined?(@ci_yaml_file)

Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
529
      @ci_yaml_file =
530
        if auto_devops_source?
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
531
          implied_ci_yaml_file
532
533
        else
          ci_yaml_from_repo
Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
534
        end
535
536
537
538
539
540

      if @ci_yaml_file
        @ci_yaml_file
      else
        self.yaml_errors = "Failed to load CI/CD config file for #{sha}"
        nil
541
      end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
542
543
    end

544
545
546
547
    def has_yaml_errors?
      yaml_errors.present?
    end

James Lopez's avatar
James Lopez committed
548
549
550
551
552
553
554
555
556
557
558
559
560
    # 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

561
    def notes
562
      project.notes.for_commit_id(sha)
563
564
    end

565
    # rubocop: disable CodeReuse/ServiceClass
566
567
568
    def process!
      Ci::ProcessPipelineService.new(project, user).execute(self)
    end
569
    # rubocop: enable CodeReuse/ServiceClass
570

571
    def update_status
572
      retry_optimistic_lock(self) do
573
574
        case latest_builds_status.to_s
        when 'created' then nil
575
576
577
578
579
580
        when 'pending' then enqueue
        when 'running' then run
        when 'success' then succeed
        when 'failed' then drop
        when 'canceled' then cancel
        when 'skipped' then skip
581
        when 'manual' then block
582
        when 'scheduled' then delay
583
584
585
        else
          raise HasStatus::UnknownStatusError,
                "Unknown status `#{latest_builds_status}`"
586
        end
587
      end
588
589
    end

590
    def protected_ref?
591
      strong_memoize(:protected_ref) { project.protected_for?(ref) }
592
593
594
    end

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

598
599
    def persisted_variables
      Gitlab::Ci::Variables::Collection.new.tap do |variables|
Kamil Trzciński's avatar
Kamil Trzciński committed
600
601
602
603
        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))
604
605
606
      end
    end

607
    def predefined_variables
608
      Gitlab::Ci::Variables::Collection.new
Kamil Trzciński's avatar
Kamil Trzciński committed
609
        .append(key: 'CI_PIPELINE_IID', value: iid.to_s)
610
611
        .append(key: 'CI_CONFIG_PATH', value: ci_yaml_file_path)
        .append(key: 'CI_PIPELINE_SOURCE', value: source.to_s)
612
613
614
        .append(key: 'CI_COMMIT_MESSAGE', value: git_commit_message.to_s)
        .append(key: 'CI_COMMIT_TITLE', value: git_commit_full_title.to_s)
        .append(key: 'CI_COMMIT_DESCRIPTION', value: git_commit_description.to_s)
615
616
    end

617
618
619
620
621
622
623
    def queued_duration
      return unless started_at

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

624
    def update_duration
625
626
      return unless started_at

627
      self.duration = Gitlab::Ci::Pipeline::Duration.from_pipeline(self)
628
629
630
    end

    def execute_hooks
Kamil Trzcinski's avatar
Kamil Trzcinski committed
631
632
633
      data = pipeline_data
      project.execute_hooks(data, :pipeline_hooks)
      project.execute_services(data, :pipeline_hooks)
634
635
    end

636
637
    # All the merge requests for which the current pipeline runs/ran against
    def all_merge_requests
Shinya Maeda's avatar
Shinya Maeda committed
638
639
640
641
642
643
      @all_merge_requests ||=
        if merge_request?
          project.merge_requests.where(id: merge_request.id)
        else
          project.merge_requests.where(source_branch: ref)
        end
644
645
    end

646
    def detailed_status(current_user)
Douwe Maan's avatar
Douwe Maan committed
647
648
649
      Gitlab::Ci::Status::Pipeline::Factory
        .new(self, current_user)
        .fabricate!
650
651
    end

652
    def latest_builds_with_artifacts
653
654
655
      # 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.
656
      @latest_builds_with_artifacts ||= builds.latest.with_artifacts_archive.to_a
657
658
    end

659
    def has_test_reports?
660
      complete? && builds.latest.with_test_reports.any?
661
662
663
664
    end

    def test_reports
      Gitlab::Ci::Reports::TestReports.new.tap do |test_reports|
665
        builds.latest.with_test_reports.each do |build|
666
667
668
669
670
          build.collect_test_reports!(test_reports)
        end
      end
    end

671
672
673
674
675
676
677
    def branch_updated?
      strong_memoize(:branch_updated) do
        push_details.branch_updated?
      end
    end

    def modified_paths
678
      strong_memoize(:modified_paths) do
679
680
681
682
        push_details.modified_paths
      end
    end

683
684
685
686
    def default_branch?
      ref == project.default_branch
    end

687
688
    private

689
    def ci_yaml_from_repo
690
691
      return unless project
      return unless sha
692
      return unless ci_yaml_file_path
693

694
      project.repository.gitlab_ci_yml_for(sha, ci_yaml_file_path)
695
    rescue GRPC::NotFound, GRPC::Internal
696
697
698
      nil
    end

Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
699
    def implied_ci_yaml_file
700
701
      return unless project

Zeger-Jan van de Weg's avatar
Zeger-Jan van de Weg committed
702
703
704
705
706
      if project.auto_devops_enabled?
        Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content
      end
    end

707
    def pipeline_data
708
      Gitlab::DataBuilder::Pipeline.build(self)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
709
    end
710

711
712
    def push_details
      strong_memoize(:push_details) do
713
        Gitlab::Git::Push.new(project, before_sha, sha, git_ref)
714
715
716
      end
    end

717
    def git_ref
718
719
      if branch?
        Gitlab::Git::BRANCH_REF_PREFIX + ref.to_s
Shinya Maeda's avatar
Shinya Maeda committed
720
721
      elsif merge_request?
        Gitlab::Git::BRANCH_REF_PREFIX + ref.to_s
722
723
724
725
726
727
728
      elsif tag?
        Gitlab::Git::TAG_REF_PREFIX + ref.to_s
      else
        raise ArgumentError, 'Invalid pipeline type!'
      end
    end

729
    def latest_builds_status
730
731
732
      return 'failed' unless yaml_errors.blank?

      statuses.latest.status || 'skipped'
Kamil Trzcinski's avatar
Kamil Trzcinski committed
733
    end
734
735

    def keep_around_commits
736
      return unless project
737

738
      project.repository.keep_around(self.sha, self.before_sha)
739
    end
740
741
742
743
744
745

    def valid_source
      if source.nil? || source == "unknown"
        errors.add(:source, "invalid source")
      end
    end
Douwe Maan's avatar
Douwe Maan committed
746
747
  end
end