pipeline.rb 14.7 KB
Newer Older
D
Douwe Maan 已提交
1
module Ci
2
  class Pipeline < ActiveRecord::Base
3
    extend Gitlab::Ci::Model
4
    include HasStatus
5
    include Importable
6
    include AfterCommitQueue
R
Rydkin Maxim 已提交
7
    include Presentable
8
    include Gitlab::OptimisticLocking
K
WIP  
Kamil Trzcinski 已提交
9

K
Kamil Trzciński 已提交
10
    belongs_to :project
11
    belongs_to :user
12
    belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline'
13
    belongs_to :pipeline_schedule, class_name: 'Ci::PipelineSchedule'
14

15
    has_many :stages
16
    has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
17
    has_many :builds, foreign_key: :commit_id
18
    has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id # rubocop:disable Cop/ActiveRecordDependent
S
init  
Shinya Maeda 已提交
19
    has_many :variables, class_name: 'Ci::PipelineVariable'
F
Felipe Artur 已提交
20 21 22

    # Merge requests for which the current pipeline is running against
    # the merge request's latest commit.
23
    has_many :merge_requests, foreign_key: "head_pipeline_id"
D
Douwe Maan 已提交
24

25
    has_many :pending_builds, -> { pending }, foreign_key: :commit_id, class_name: 'Ci::Build'
S
Shinya Maeda 已提交
26
    has_many :retryable_builds, -> { latest.failed_or_canceled.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build'
27
    has_many :cancelable_statuses, -> { cancelable }, foreign_key: :commit_id, class_name: 'CommitStatus'
28 29
    has_many :manual_actions, -> { latest.manual_actions.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build'
    has_many :artifacts, -> { latest.with_artifacts_not_expired.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build'
30

31 32
    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'
33

D
Douwe Maan 已提交
34
    delegate :id, to: :project, prefix: true
35
    delegate :full_path, to: :project, prefix: true
D
Douwe Maan 已提交
36

37
    validates :source, exclusion: { in: %w(unknown), unless: :importing? }, on: :create
D
Douwe Maan 已提交
38 39 40
    validates :sha, presence: { unless: :importing? }
    validates :ref, presence: { unless: :importing? }
    validates :status, presence: { unless: :importing? }
41
    validate :valid_commit_sha, unless: :importing?
D
Douwe Maan 已提交
42

43
    after_create :keep_around_commits, unless: :importing?
K
Kamil Trzcinski 已提交
44

45 46 47 48 49 50 51 52 53 54
    enum source: {
      unknown: nil,
      push: 1,
      web: 2,
      trigger: 3,
      schedule: 4,
      api: 5,
      external: 6
    }

55
    enum config_source: {
Z
Zeger-Jan van de Weg 已提交
56 57 58
      unknown_source: nil,
      repository_source: 1,
      auto_devops_source: 2
59 60
    }

61
    enum failure_reason: {
62 63
      unknown_failure: 0,
      config_error: 1
64 65
    }

66
    state_machine :status, initial: :created do
67
      event :enqueue do
68 69
        transition [:created, :skipped] => :pending
        transition [:success, :failed, :canceled] => :running
70 71 72
      end

      event :run do
K
Kamil Trzcinski 已提交
73
        transition any - [:running] => :running
74 75
      end

76
      event :skip do
K
Kamil Trzcinski 已提交
77
        transition any - [:skipped] => :skipped
78 79 80
      end

      event :drop do
K
Kamil Trzcinski 已提交
81
        transition any - [:failed] => :failed
82 83
      end

84
      event :succeed do
K
Kamil Trzcinski 已提交
85
        transition any - [:success] => :success
86 87 88
      end

      event :cancel do
K
Kamil Trzcinski 已提交
89
        transition any - [:canceled] => :canceled
90 91
      end

92
      event :block do
93
        transition any - [:manual] => :manual
94 95
      end

K
Kamil Trzcinski 已提交
96 97 98 99
      # IMPORTANT
      # Do not add any operations to this state_machine
      # Create a separate worker for each new operation

100
      before_transition [:created, :pending] => :running do |pipeline|
101
        pipeline.started_at = Time.now
102 103
      end

104
      before_transition any => [:success, :failed, :canceled] do |pipeline|
105
        pipeline.finished_at = Time.now
106 107 108
        pipeline.update_duration
      end

109 110 111 112
      before_transition any => [:manual] do |pipeline|
        pipeline.update_duration
      end

L
Lin Jen-Shin 已提交
113
      before_transition canceled: any - [:canceled] do |pipeline|
L
Lin Jen-Shin 已提交
114 115 116
        pipeline.auto_canceled_by = nil
      end

117 118 119 120 121 122
      before_transition any => :failed do |pipeline, transition|
        transition.args.first.try do |reason|
          pipeline.failure_reason = reason
        end
      end

123
      after_transition [:created, :pending] => :running do |pipeline|
124
        pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
125 126 127
      end

      after_transition any => [:success] do |pipeline|
128
        pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
129 130
      end

131
      after_transition [:created, :pending, :running] => :success do |pipeline|
132
        pipeline.run_after_commit { PipelineSuccessWorker.perform_async(pipeline.id) }
133
      end
134 135

      after_transition do |pipeline, transition|
136 137 138
        next if transition.loopback?

        pipeline.run_after_commit do
139
          PipelineHooksWorker.perform_async(pipeline.id)
140
          ExpirePipelineCacheWorker.perform_async(pipeline.id)
141
        end
142
      end
143

144
      after_transition any => [:success, :failed] do |pipeline|
145
        pipeline.run_after_commit do
146
          PipelineNotificationWorker.perform_async(pipeline.id)
147
        end
148
      end
149 150
    end

151
    scope :internal, -> { where(source: internal_sources) }
152

153 154 155 156 157 158 159 160 161
    # 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.
    def self.newest_first(ref = nil)
      relation = order(id: :desc)

      ref ? relation.where(ref: ref) : relation
162
    end
163

164
    def self.latest_status(ref = nil)
165
      newest_first(ref).pluck(:status).first
166 167
    end

168
    def self.latest_successful_for(ref)
169
      newest_first(ref).success.take
170 171
    end

172
    def self.latest_successful_for_refs(refs)
173 174 175
      relation = newest_first(refs).success

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

180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
    # 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

D
Douwe Maan 已提交
215 216 217 218
    def self.truncate_sha(sha)
      sha[0...8]
    end

219
    def self.total_duration
L
Lin Jen-Shin 已提交
220
      where.not(duration: nil).sum(:duration)
221 222
    end

223 224 225 226
    def self.internal_sources
      sources.reject { |source| source == "external" }.values
    end

227 228
    def stages_count
      statuses.select(:stage).distinct.count
K
Kamil Trzcinski 已提交
229 230
    end

231 232 233 234
    def total_size
      statuses.count(:id)
    end

235
    def stages_names
236 237
      statuses.order(:stage_idx).distinct
        .pluck(:stage, :stage_idx).map(&:first)
K
Kamil Trzcinski 已提交
238 239
    end

240
    def legacy_stage(name)
241
      stage = Ci::LegacyStage.new(self, name: name)
242 243 244 245
      stage unless stage.statuses_count.zero?
    end

    def legacy_stages
246 247
      # TODO, this needs refactoring, see gitlab-ce#26481.

D
Douwe Maan 已提交
248 249
      stages_query = statuses
        .group('stage').select(:stage).order('max(stage_idx)')
250

K
Kamil Trzcinski 已提交
251 252
      status_sql = statuses.latest.where('stage=sg.stage').status_sql

253
      warnings_sql = statuses.latest.select('COUNT(*)')
D
Douwe Maan 已提交
254
        .where('stage=sg.stage').failed_but_allowed.to_sql
255

D
Douwe Maan 已提交
256 257
      stages_with_statuses = CommitStatus.from(stages_query, :sg)
        .pluck('sg.stage', status_sql, "(#{warnings_sql})")
K
Kamil Trzcinski 已提交
258 259

      stages_with_statuses.map do |stage|
260
        Ci::LegacyStage.new(self, Hash[%i[name status warnings].zip(stage)])
K
Kamil Trzcinski 已提交
261 262 263
      end
    end

D
Douwe Maan 已提交
264
    def valid_commit_sha
265
      if self.sha == Gitlab::Git::BLANK_SHA
D
Douwe Maan 已提交
266 267 268 269 270
        self.errors.add(:sha, " cant be 00000000 (branch removal)")
      end
    end

    def git_author_name
271
      commit.try(:author_name)
D
Douwe Maan 已提交
272 273 274
    end

    def git_author_email
275
      commit.try(:author_email)
D
Douwe Maan 已提交
276 277 278
    end

    def git_commit_message
279
      commit.try(:message)
D
Douwe Maan 已提交
280 281
    end

282 283 284 285
    def git_commit_title
      commit.try(:title)
    end

D
Douwe Maan 已提交
286
    def short_sha
287
      Ci::Pipeline.truncate_sha(sha)
D
Douwe Maan 已提交
288 289
    end

290
    def commit
291
      @commit ||= project.commit_by(oid: sha)
D
Douwe Maan 已提交
292 293
    end

K
Kamil Trzcinski 已提交
294 295 296 297
    def branch?
      !tag?
    end

298
    def stuck?
299
      pending_builds.any?(&:stuck?)
300 301
    end

K
Kamil Trzcinski 已提交
302
    def retryable?
303
      retryable_builds.any?
K
Kamil Trzcinski 已提交
304 305
    end

306
    def cancelable?
307
      cancelable_statuses.any?
308 309
    end

L
Lin Jen-Shin 已提交
310 311 312 313
    def auto_canceled?
      canceled? && auto_canceled_by_id?
    end

K
Kamil Trzcinski 已提交
314
    def cancel_running
315
      retry_optimistic_lock(cancelable_statuses) do |cancelable|
L
Lin Jen-Shin 已提交
316 317 318
        cancelable.find_each do |job|
          yield(job) if block_given?
          job.cancel
319
        end
L
Lin Jen-Shin 已提交
320
      end
K
Kamil Trzcinski 已提交
321 322
    end

323 324 325 326 327
    def auto_cancel_running(pipeline)
      update(auto_canceled_by: pipeline)

      cancel_running do |job|
        job.auto_canceled_by = pipeline
328
      end
K
Kamil Trzcinski 已提交
329 330
    end

331
    def retry_failed(current_user)
D
Douwe Maan 已提交
332 333
      Ci::RetryPipelineService.new(project, current_user)
        .execute(self)
K
Kamil Trzcinski 已提交
334 335
    end

336
    def mark_as_processable_after_stage(stage_idx)
337
      builds.skipped.after_stage(stage_idx).find_each(&:process)
338 339
    end

K
Kamil Trzcinski 已提交
340 341
    def latest?
      return false unless ref
342

K
Kamil Trzcinski 已提交
343 344
      commit = project.commit(ref)
      return false unless commit
345

K
Kamil Trzcinski 已提交
346 347 348
      commit.sha == sha
    end

K
Kamil Trzcinski 已提交
349 350
    def retried
      @retried ||= (statuses.order(id: :desc) - statuses.latest)
D
Douwe Maan 已提交
351 352 353
    end

    def coverage
354
      coverage_array = statuses.latest.map(&:coverage).compact
K
Kamil Trzcinski 已提交
355 356
      if coverage_array.size >= 1
        '%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
D
Douwe Maan 已提交
357 358 359
      end
    end

360
    def stage_seeds
361 362
      return [] unless config_processor

363
      @stage_seeds ||= config_processor.stage_seeds(self)
364 365
    end

366 367 368 369
    def seeds_size
      @seeds_size ||= stage_seeds.sum(&:size)
    end

370
    def has_kubernetes_active?
371
      project.deployment_platform&.active?
372 373
    end

374 375
    def has_stage_seeds?
      stage_seeds.any?
376 377
    end

C
Connor Shea 已提交
378
    def has_warnings?
379
      builds.latest.failed_but_allowed.any?
380 381
    end

382
    def set_config_source
383 384 385 386 387
      if ci_yaml_from_repo
        self.config_source = :repository_source
      elsif implied_ci_yaml_file
        self.config_source = :auto_devops_source
      end
Z
Zeger-Jan van de Weg 已提交
388 389
    end

D
Douwe Maan 已提交
390
    def config_processor
391
      return unless ci_yaml_file
392 393 394
      return @config_processor if defined?(@config_processor)

      @config_processor ||= begin
395
        Gitlab::Ci::YamlProcessor.new(ci_yaml_file)
396
      rescue Gitlab::Ci::YamlProcessor::ValidationError, Psych::SyntaxError => e
397
        self.yaml_errors = e.message
398 399
        nil
      rescue
400
        self.yaml_errors = 'Undefined error'
401 402
        nil
      end
D
Douwe Maan 已提交
403 404
    end

405
    def ci_yaml_file_path
406
      if project.ci_config_path.blank?
407 408
        '.gitlab-ci.yml'
      else
409
        project.ci_config_path
410 411 412
      end
    end

K
Kamil Trzcinski 已提交
413
    def ci_yaml_file
414 415
      return @ci_yaml_file if defined?(@ci_yaml_file)

Z
Zeger-Jan van de Weg 已提交
416
      @ci_yaml_file =
417
        if auto_devops_source?
Z
Zeger-Jan van de Weg 已提交
418
          implied_ci_yaml_file
419 420
        else
          ci_yaml_from_repo
Z
Zeger-Jan van de Weg 已提交
421
        end
422 423 424 425 426 427

      if @ci_yaml_file
        @ci_yaml_file
      else
        self.yaml_errors = "Failed to load CI/CD config file for #{sha}"
        nil
428
      end
K
Kamil Trzcinski 已提交
429 430
    end

431 432 433 434
    def has_yaml_errors?
      yaml_errors.present?
    end

K
Kamil Trzcinski 已提交
435 436 437 438
    def environments
      builds.where.not(environment: nil).success.pluck(:environment).uniq
    end

J
James Lopez 已提交
439 440 441 442 443 444 445 446 447 448 449 450 451
    # 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

452
    def notes
453
      project.notes.for_commit_id(sha)
454 455
    end

456 457 458
    def process!
      Ci::ProcessPipelineService.new(project, user).execute(self)
    end
459

460
    def update_status
461
      retry_optimistic_lock(self) do
K
Kamil Trzcinski 已提交
462
        case latest_builds_status
463 464 465 466 467 468
        when 'pending' then enqueue
        when 'running' then run
        when 'success' then succeed
        when 'failed' then drop
        when 'canceled' then cancel
        when 'skipped' then skip
469
        when 'manual' then block
470
        end
471
      end
472 473
    end

474 475
    def predefined_variables
      [
L
Lin Jen-Shin 已提交
476
        { key: 'CI_PIPELINE_ID', value: id.to_s, public: true },
477 478
        { key: 'CI_CONFIG_PATH', value: ci_yaml_file_path, public: true },
        { key: 'CI_PIPELINE_SOURCE', value: source.to_s, public: true }
479 480 481
      ]
    end

482 483 484 485 486 487 488
    def queued_duration
      return unless started_at

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

489
    def update_duration
490 491
      return unless started_at

492
      self.duration = Gitlab::Ci::Pipeline::Duration.from_pipeline(self)
493 494 495
    end

    def execute_hooks
K
Kamil Trzcinski 已提交
496 497 498
      data = pipeline_data
      project.execute_hooks(data, :pipeline_hooks)
      project.execute_services(data, :pipeline_hooks)
499 500
    end

501 502
    # All the merge requests for which the current pipeline runs/ran against
    def all_merge_requests
503
      @all_merge_requests ||= project.merge_requests.where(source_branch: ref)
504 505
    end

506
    def detailed_status(current_user)
D
Douwe Maan 已提交
507 508 509
      Gitlab::Ci::Status::Pipeline::Factory
        .new(self, current_user)
        .fabricate!
510 511
    end

512
    def latest_builds_with_artifacts
513 514 515 516
      # 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.to_a
517 518
    end

519 520
    private

521
    def ci_yaml_from_repo
522 523 524
      return unless project
      return unless sha

525 526 527 528 529
      project.repository.gitlab_ci_yml_for(sha, ci_yaml_file_path)
    rescue GRPC::NotFound, Rugged::ReferenceError, GRPC::Internal
      nil
    end

Z
Zeger-Jan van de Weg 已提交
530
    def implied_ci_yaml_file
531 532
      return unless project

Z
Zeger-Jan van de Weg 已提交
533 534 535 536 537
      if project.auto_devops_enabled?
        Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content
      end
    end

538
    def pipeline_data
539
      Gitlab::DataBuilder::Pipeline.build(self)
K
Kamil Trzcinski 已提交
540
    end
541

542
    def latest_builds_status
543 544 545
      return 'failed' unless yaml_errors.blank?

      statuses.latest.status || 'skipped'
K
Kamil Trzcinski 已提交
546
    end
547 548

    def keep_around_commits
549
      return unless project
550

551 552 553
      project.repository.keep_around(self.sha)
      project.repository.keep_around(self.before_sha)
    end
D
Douwe Maan 已提交
554 555
  end
end