pipeline.rb 13.2 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
K
WIP  
Kamil Trzcinski 已提交
8

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

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

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

24
    has_many :pending_builds, -> { pending }, foreign_key: :commit_id, class_name: 'Ci::Build'
S
Shinya Maeda 已提交
25
    has_many :retryable_builds, -> { latest.failed_or_canceled.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build'
26
    has_many :cancelable_statuses, -> { cancelable }, foreign_key: :commit_id, class_name: 'CommitStatus'
27 28
    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'
29

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

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

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

42
    after_initialize :set_config_source, if: :new_record?
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
K
Kamil Trzcinski 已提交
68
        transition created: :pending
69
        transition [:success, :failed, :canceled, :skipped] => :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
    # ref can't be HEAD or SHA, can only be branch/tag name
152
    scope :latest, ->(ref = nil) do
D
Douwe Maan 已提交
153 154 155
      max_id = unscope(:select)
        .select("max(#{quoted_table_name}.id)")
        .group(:ref, :sha)
156

157 158 159 160 161
      if ref
        where(ref: ref, id: max_id.where(ref: ref))
      else
        where(id: max_id)
      end
162
    end
163
    scope :internal, -> { where(source: internal_sources) }
164

165 166 167 168
    def self.latest_status(ref = nil)
      latest(ref).status
    end

169
    def self.latest_successful_for(ref)
170
      success.latest(ref).order(id: :desc).first
171 172
    end

173 174 175 176 177 178
    def self.latest_successful_for_refs(refs)
      success.latest(refs).order(id: :desc).each_with_object({}) do |pipeline, hash|
        hash[pipeline.ref] ||= pipeline
      end
    end

D
Douwe Maan 已提交
179 180 181 182
    def self.truncate_sha(sha)
      sha[0...8]
    end

183
    def self.total_duration
L
Lin Jen-Shin 已提交
184
      where.not(duration: nil).sum(:duration)
185 186
    end

187 188 189 190
    def self.internal_sources
      sources.reject { |source| source == "external" }.values
    end

191 192
    def stages_count
      statuses.select(:stage).distinct.count
K
Kamil Trzcinski 已提交
193 194
    end

195
    def stages_names
196 197
      statuses.order(:stage_idx).distinct
        .pluck(:stage, :stage_idx).map(&:first)
K
Kamil Trzcinski 已提交
198 199
    end

200
    def legacy_stage(name)
201
      stage = Ci::LegacyStage.new(self, name: name)
202 203 204 205
      stage unless stage.statuses_count.zero?
    end

    def legacy_stages
206 207
      # TODO, this needs refactoring, see gitlab-ce#26481.

D
Douwe Maan 已提交
208 209
      stages_query = statuses
        .group('stage').select(:stage).order('max(stage_idx)')
210

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

213
      warnings_sql = statuses.latest.select('COUNT(*)')
D
Douwe Maan 已提交
214
        .where('stage=sg.stage').failed_but_allowed.to_sql
215

D
Douwe Maan 已提交
216 217
      stages_with_statuses = CommitStatus.from(stages_query, :sg)
        .pluck('sg.stage', status_sql, "(#{warnings_sql})")
K
Kamil Trzcinski 已提交
218 219

      stages_with_statuses.map do |stage|
220
        Ci::LegacyStage.new(self, Hash[%i[name status warnings].zip(stage)])
K
Kamil Trzcinski 已提交
221 222 223
      end
    end

D
Douwe Maan 已提交
224
    def valid_commit_sha
225
      if self.sha == Gitlab::Git::BLANK_SHA
D
Douwe Maan 已提交
226 227 228 229 230
        self.errors.add(:sha, " cant be 00000000 (branch removal)")
      end
    end

    def git_author_name
231
      commit.try(:author_name)
D
Douwe Maan 已提交
232 233 234
    end

    def git_author_email
235
      commit.try(:author_email)
D
Douwe Maan 已提交
236 237 238
    end

    def git_commit_message
239
      commit.try(:message)
D
Douwe Maan 已提交
240 241
    end

242 243 244 245
    def git_commit_title
      commit.try(:title)
    end

D
Douwe Maan 已提交
246
    def short_sha
247
      Ci::Pipeline.truncate_sha(sha)
D
Douwe Maan 已提交
248 249
    end

250
    def commit
K
Kamil Trzcinski 已提交
251
      @commit ||= project.commit(sha)
D
Douwe Maan 已提交
252 253 254 255
    rescue
      nil
    end

K
Kamil Trzcinski 已提交
256 257 258 259
    def branch?
      !tag?
    end

260
    def stuck?
261
      pending_builds.any?(&:stuck?)
262 263
    end

K
Kamil Trzcinski 已提交
264
    def retryable?
265
      retryable_builds.any?
K
Kamil Trzcinski 已提交
266 267
    end

268
    def cancelable?
269
      cancelable_statuses.any?
270 271
    end

L
Lin Jen-Shin 已提交
272 273 274 275
    def auto_canceled?
      canceled? && auto_canceled_by_id?
    end

K
Kamil Trzcinski 已提交
276
    def cancel_running
277
      Gitlab::OptimisticLocking.retry_lock(cancelable_statuses) do |cancelable|
L
Lin Jen-Shin 已提交
278 279 280
        cancelable.find_each do |job|
          yield(job) if block_given?
          job.cancel
281
        end
L
Lin Jen-Shin 已提交
282
      end
K
Kamil Trzcinski 已提交
283 284
    end

285 286 287 288 289
    def auto_cancel_running(pipeline)
      update(auto_canceled_by: pipeline)

      cancel_running do |job|
        job.auto_canceled_by = pipeline
290
      end
K
Kamil Trzcinski 已提交
291 292
    end

293
    def retry_failed(current_user)
D
Douwe Maan 已提交
294 295
      Ci::RetryPipelineService.new(project, current_user)
        .execute(self)
K
Kamil Trzcinski 已提交
296 297
    end

298
    def mark_as_processable_after_stage(stage_idx)
299
      builds.skipped.after_stage(stage_idx).find_each(&:process)
300 301
    end

K
Kamil Trzcinski 已提交
302 303 304 305 306 307 308
    def latest?
      return false unless ref
      commit = project.commit(ref)
      return false unless commit
      commit.sha == sha
    end

K
Kamil Trzcinski 已提交
309 310
    def retried
      @retried ||= (statuses.order(id: :desc) - statuses.latest)
D
Douwe Maan 已提交
311 312 313
    end

    def coverage
314
      coverage_array = statuses.latest.map(&:coverage).compact
K
Kamil Trzcinski 已提交
315 316
      if coverage_array.size >= 1
        '%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
D
Douwe Maan 已提交
317 318 319
      end
    end

320
    def stage_seeds
321 322
      return [] unless config_processor

323
      @stage_seeds ||= config_processor.stage_seeds(self)
324 325
    end

326 327 328 329
    def seeds_size
      @seeds_size ||= stage_seeds.sum(&:size)
    end

330 331
    def has_kubernetes_active?
      project.kubernetes_service&.active?
332 333
    end

334 335
    def has_stage_seeds?
      stage_seeds.any?
336 337
    end

C
Connor Shea 已提交
338
    def has_warnings?
339
      builds.latest.failed_but_allowed.any?
340 341
    end

342
    def set_config_source
343 344 345 346 347
      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 已提交
348 349
    end

D
Douwe Maan 已提交
350
    def config_processor
351
      return unless ci_yaml_file
352 353 354
      return @config_processor if defined?(@config_processor)

      @config_processor ||= begin
355
        Gitlab::Ci::YamlProcessor.new(ci_yaml_file)
356
      rescue Gitlab::Ci::YamlProcessor::ValidationError, Psych::SyntaxError => e
357
        self.yaml_errors = e.message
358 359
        nil
      rescue
360
        self.yaml_errors = 'Undefined error'
361 362
        nil
      end
D
Douwe Maan 已提交
363 364
    end

365
    def ci_yaml_file_path
366
      if project.ci_config_path.blank?
367 368
        '.gitlab-ci.yml'
      else
369
        project.ci_config_path
370 371 372
      end
    end

K
Kamil Trzcinski 已提交
373
    def ci_yaml_file
374 375
      return @ci_yaml_file if defined?(@ci_yaml_file)

Z
Zeger-Jan van de Weg 已提交
376
      @ci_yaml_file =
377
        if auto_devops_source?
Z
Zeger-Jan van de Weg 已提交
378
          implied_ci_yaml_file
379 380
        else
          ci_yaml_from_repo
Z
Zeger-Jan van de Weg 已提交
381
        end
382 383 384 385 386 387

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

391 392 393 394
    def has_yaml_errors?
      yaml_errors.present?
    end

K
Kamil Trzcinski 已提交
395 396 397 398
    def environments
      builds.where.not(environment: nil).success.pluck(:environment).uniq
    end

J
James Lopez 已提交
399 400 401 402 403 404 405 406 407 408 409 410 411
    # 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

412 413 414 415
    def notes
      Note.for_commit_id(sha)
    end

416 417 418
    def process!
      Ci::ProcessPipelineService.new(project, user).execute(self)
    end
419

420
    def update_status
K
Kamil Trzcinski 已提交
421
      Gitlab::OptimisticLocking.retry_lock(self) do
K
Kamil Trzcinski 已提交
422
        case latest_builds_status
423 424 425 426 427 428
        when 'pending' then enqueue
        when 'running' then run
        when 'success' then succeed
        when 'failed' then drop
        when 'canceled' then cancel
        when 'skipped' then skip
429
        when 'manual' then block
430
        end
431
      end
432 433
    end

434 435
    def predefined_variables
      [
L
Lin Jen-Shin 已提交
436
        { key: 'CI_PIPELINE_ID', value: id.to_s, public: true },
437 438
        { key: 'CI_CONFIG_PATH', value: ci_yaml_file_path, public: true },
        { key: 'CI_PIPELINE_SOURCE', value: source.to_s, public: true }
439 440 441
      ]
    end

442 443 444 445 446 447 448
    def queued_duration
      return unless started_at

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

449
    def update_duration
450 451
      return unless started_at

452
      self.duration = Gitlab::Ci::Pipeline::Duration.from_pipeline(self)
453 454 455
    end

    def execute_hooks
K
Kamil Trzcinski 已提交
456 457 458
      data = pipeline_data
      project.execute_hooks(data, :pipeline_hooks)
      project.execute_services(data, :pipeline_hooks)
459 460
    end

461 462
    # All the merge requests for which the current pipeline runs/ran against
    def all_merge_requests
463
      @all_merge_requests ||= project.merge_requests.where(source_branch: ref)
464 465
    end

466
    def detailed_status(current_user)
D
Douwe Maan 已提交
467 468 469
      Gitlab::Ci::Status::Pipeline::Factory
        .new(self, current_user)
        .fabricate!
470 471
    end

472 473 474 475
    def latest_builds_with_artifacts
      @latest_builds_with_artifacts ||= builds.latest.with_artifacts
    end

476 477
    private

478
    def ci_yaml_from_repo
479 480 481
      return unless project
      return unless sha

482 483 484 485 486
      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 已提交
487
    def implied_ci_yaml_file
488 489
      return unless project

Z
Zeger-Jan van de Weg 已提交
490 491 492 493 494
      if project.auto_devops_enabled?
        Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content
      end
    end

495
    def pipeline_data
496
      Gitlab::DataBuilder::Pipeline.build(self)
K
Kamil Trzcinski 已提交
497
    end
498

499
    def latest_builds_status
500 501 502
      return 'failed' unless yaml_errors.blank?

      statuses.latest.status || 'skipped'
K
Kamil Trzcinski 已提交
503
    end
504 505

    def keep_around_commits
506
      return unless project
507

508 509 510
      project.repository.keep_around(self.sha)
      project.repository.keep_around(self.before_sha)
    end
D
Douwe Maan 已提交
511 512
  end
end