pipeline.rb 14.5 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_initialize :set_config_source, if: :new_record?
44
    after_create :keep_around_commits, unless: :importing?
K
Kamil Trzcinski 已提交
45

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

154 155 156 157 158 159 160 161 162
    # 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
163
    end
164

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

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

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

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

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 215
    # 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 已提交
216 217 218 219
    def self.truncate_sha(sha)
      sha[0...8]
    end

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

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

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

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

237
    def legacy_stage(name)
238
      stage = Ci::LegacyStage.new(self, name: name)
239 240 241 242
      stage unless stage.statuses_count.zero?
    end

    def legacy_stages
243 244
      # TODO, this needs refactoring, see gitlab-ce#26481.

D
Douwe Maan 已提交
245 246
      stages_query = statuses
        .group('stage').select(:stage).order('max(stage_idx)')
247

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

250
      warnings_sql = statuses.latest.select('COUNT(*)')
D
Douwe Maan 已提交
251
        .where('stage=sg.stage').failed_but_allowed.to_sql
252

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

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

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

    def git_author_name
268
      commit.try(:author_name)
D
Douwe Maan 已提交
269 270 271
    end

    def git_author_email
272
      commit.try(:author_email)
D
Douwe Maan 已提交
273 274 275
    end

    def git_commit_message
276
      commit.try(:message)
D
Douwe Maan 已提交
277 278
    end

279 280 281 282
    def git_commit_title
      commit.try(:title)
    end

D
Douwe Maan 已提交
283
    def short_sha
284
      Ci::Pipeline.truncate_sha(sha)
D
Douwe Maan 已提交
285 286
    end

287
    def commit
288
      @commit ||= project.commit_by(oid: sha)
D
Douwe Maan 已提交
289 290
    end

K
Kamil Trzcinski 已提交
291 292 293 294
    def branch?
      !tag?
    end

295
    def stuck?
296
      pending_builds.any?(&:stuck?)
297 298
    end

K
Kamil Trzcinski 已提交
299
    def retryable?
300
      retryable_builds.any?
K
Kamil Trzcinski 已提交
301 302
    end

303
    def cancelable?
304
      cancelable_statuses.any?
305 306
    end

L
Lin Jen-Shin 已提交
307 308 309 310
    def auto_canceled?
      canceled? && auto_canceled_by_id?
    end

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

320 321 322 323 324
    def auto_cancel_running(pipeline)
      update(auto_canceled_by: pipeline)

      cancel_running do |job|
        job.auto_canceled_by = pipeline
325
      end
K
Kamil Trzcinski 已提交
326 327
    end

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

333
    def mark_as_processable_after_stage(stage_idx)
334
      builds.skipped.after_stage(stage_idx).find_each(&:process)
335 336
    end

K
Kamil Trzcinski 已提交
337 338
    def latest?
      return false unless ref
339

K
Kamil Trzcinski 已提交
340 341
      commit = project.commit(ref)
      return false unless commit
342

K
Kamil Trzcinski 已提交
343 344 345
      commit.sha == sha
    end

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

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

357
    def stage_seeds
358 359
      return [] unless config_processor

360
      @stage_seeds ||= config_processor.stage_seeds(self)
361 362
    end

363 364 365 366
    def seeds_size
      @seeds_size ||= stage_seeds.sum(&:size)
    end

367 368
    def has_kubernetes_active?
      project.kubernetes_service&.active?
369 370
    end

371 372
    def has_stage_seeds?
      stage_seeds.any?
373 374
    end

C
Connor Shea 已提交
375
    def has_warnings?
376
      builds.latest.failed_but_allowed.any?
377 378
    end

379
    def set_config_source
380 381 382 383 384
      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 已提交
385 386
    end

D
Douwe Maan 已提交
387
    def config_processor
388
      return unless ci_yaml_file
389 390 391
      return @config_processor if defined?(@config_processor)

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

402
    def ci_yaml_file_path
403
      if project.ci_config_path.blank?
404 405
        '.gitlab-ci.yml'
      else
406
        project.ci_config_path
407 408 409
      end
    end

K
Kamil Trzcinski 已提交
410
    def ci_yaml_file
411 412
      return @ci_yaml_file if defined?(@ci_yaml_file)

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

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

428 429 430 431
    def has_yaml_errors?
      yaml_errors.present?
    end

K
Kamil Trzcinski 已提交
432 433 434 435
    def environments
      builds.where.not(environment: nil).success.pluck(:environment).uniq
    end

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

449
    def notes
450
      project.notes.for_commit_id(sha)
451 452
    end

453 454 455
    def process!
      Ci::ProcessPipelineService.new(project, user).execute(self)
    end
456

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

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

479 480 481 482 483 484 485
    def queued_duration
      return unless started_at

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

486
    def update_duration
487 488
      return unless started_at

489
      self.duration = Gitlab::Ci::Pipeline::Duration.from_pipeline(self)
490 491 492
    end

    def execute_hooks
K
Kamil Trzcinski 已提交
493 494 495
      data = pipeline_data
      project.execute_hooks(data, :pipeline_hooks)
      project.execute_services(data, :pipeline_hooks)
496 497
    end

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

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

509 510 511 512
    def latest_builds_with_artifacts
      @latest_builds_with_artifacts ||= builds.latest.with_artifacts
    end

513 514
    private

515
    def ci_yaml_from_repo
516 517 518
      return unless project
      return unless sha

519 520 521 522 523
      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 已提交
524
    def implied_ci_yaml_file
525 526
      return unless project

Z
Zeger-Jan van de Weg 已提交
527 528 529 530 531
      if project.auto_devops_enabled?
        Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content
      end
    end

532
    def pipeline_data
533
      Gitlab::DataBuilder::Pipeline.build(self)
K
Kamil Trzcinski 已提交
534
    end
535

536
    def latest_builds_status
537 538 539
      return 'failed' unless yaml_errors.blank?

      statuses.latest.status || 'skipped'
K
Kamil Trzcinski 已提交
540
    end
541 542

    def keep_around_commits
543
      return unless project
544

545 546 547
      project.repository.keep_around(self.sha)
      project.repository.keep_around(self.before_sha)
    end
D
Douwe Maan 已提交
548 549
  end
end