pipeline.rb 12.7 KB
Newer Older
D
Douwe Maan 已提交
1
module Ci
2
  class Pipeline < ActiveRecord::Base
D
Douwe Maan 已提交
3
    extend 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 34
    delegate :id, to: :project, prefix: true

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

41
    after_initialize :set_config_source, if: :new_record?
42
    after_create :keep_around_commits, unless: :importing?
K
Kamil Trzcinski 已提交
43

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

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

60
    state_machine :status, initial: :created do
61
      event :enqueue do
K
Kamil Trzcinski 已提交
62
        transition created: :pending
63
        transition [:success, :failed, :canceled, :skipped] => :running
64 65 66
      end

      event :run do
K
Kamil Trzcinski 已提交
67
        transition any - [:running] => :running
68 69
      end

70
      event :skip do
K
Kamil Trzcinski 已提交
71
        transition any - [:skipped] => :skipped
72 73 74
      end

      event :drop do
K
Kamil Trzcinski 已提交
75
        transition any - [:failed] => :failed
76 77
      end

78
      event :succeed do
K
Kamil Trzcinski 已提交
79
        transition any - [:success] => :success
80 81 82
      end

      event :cancel do
K
Kamil Trzcinski 已提交
83
        transition any - [:canceled] => :canceled
84 85
      end

86
      event :block do
87
        transition any - [:manual] => :manual
88 89
      end

K
Kamil Trzcinski 已提交
90 91 92 93
      # IMPORTANT
      # Do not add any operations to this state_machine
      # Create a separate worker for each new operation

94
      before_transition [:created, :pending] => :running do |pipeline|
95
        pipeline.started_at = Time.now
96 97
      end

98
      before_transition any => [:success, :failed, :canceled] do |pipeline|
99
        pipeline.finished_at = Time.now
100 101 102
        pipeline.update_duration
      end

103 104 105 106
      before_transition any => [:manual] do |pipeline|
        pipeline.update_duration
      end

L
Lin Jen-Shin 已提交
107
      before_transition canceled: any - [:canceled] do |pipeline|
L
Lin Jen-Shin 已提交
108 109 110
        pipeline.auto_canceled_by = nil
      end

111
      after_transition [:created, :pending] => :running do |pipeline|
112
        pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
113 114 115
      end

      after_transition any => [:success] do |pipeline|
116
        pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
117 118
      end

119
      after_transition [:created, :pending, :running] => :success do |pipeline|
120
        pipeline.run_after_commit { PipelineSuccessWorker.perform_async(pipeline.id) }
121
      end
122 123

      after_transition do |pipeline, transition|
124 125 126
        next if transition.loopback?

        pipeline.run_after_commit do
127
          PipelineHooksWorker.perform_async(pipeline.id)
128
          ExpirePipelineCacheWorker.perform_async(pipeline.id)
129
        end
130
      end
131

132
      after_transition any => [:success, :failed] do |pipeline|
133
        pipeline.run_after_commit do
134
          PipelineNotificationWorker.perform_async(pipeline.id)
135
        end
136
      end
137 138
    end

139
    # ref can't be HEAD or SHA, can only be branch/tag name
140
    scope :latest, ->(ref = nil) do
D
Douwe Maan 已提交
141 142 143
      max_id = unscope(:select)
        .select("max(#{quoted_table_name}.id)")
        .group(:ref, :sha)
144

145 146 147 148 149
      if ref
        where(ref: ref, id: max_id.where(ref: ref))
      else
        where(id: max_id)
      end
150
    end
151
    scope :internal, -> { where(source: internal_sources) }
152

153 154 155 156
    def self.latest_status(ref = nil)
      latest(ref).status
    end

157
    def self.latest_successful_for(ref)
158
      success.latest(ref).order(id: :desc).first
159 160
    end

161 162 163 164 165 166
    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 已提交
167 168 169 170
    def self.truncate_sha(sha)
      sha[0...8]
    end

171
    def self.total_duration
L
Lin Jen-Shin 已提交
172
      where.not(duration: nil).sum(:duration)
173 174
    end

175 176 177 178
    def self.internal_sources
      sources.reject { |source| source == "external" }.values
    end

179 180
    def stages_count
      statuses.select(:stage).distinct.count
K
Kamil Trzcinski 已提交
181 182
    end

183
    def stages_names
184 185
      statuses.order(:stage_idx).distinct
        .pluck(:stage, :stage_idx).map(&:first)
K
Kamil Trzcinski 已提交
186 187
    end

188
    def legacy_stage(name)
189
      stage = Ci::LegacyStage.new(self, name: name)
190 191 192 193
      stage unless stage.statuses_count.zero?
    end

    def legacy_stages
194 195
      # TODO, this needs refactoring, see gitlab-ce#26481.

D
Douwe Maan 已提交
196 197
      stages_query = statuses
        .group('stage').select(:stage).order('max(stage_idx)')
198

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

201
      warnings_sql = statuses.latest.select('COUNT(*)')
D
Douwe Maan 已提交
202
        .where('stage=sg.stage').failed_but_allowed.to_sql
203

D
Douwe Maan 已提交
204 205
      stages_with_statuses = CommitStatus.from(stages_query, :sg)
        .pluck('sg.stage', status_sql, "(#{warnings_sql})")
K
Kamil Trzcinski 已提交
206 207

      stages_with_statuses.map do |stage|
208
        Ci::LegacyStage.new(self, Hash[%i[name status warnings].zip(stage)])
K
Kamil Trzcinski 已提交
209 210 211
      end
    end

D
Douwe Maan 已提交
212
    def valid_commit_sha
213
      if self.sha == Gitlab::Git::BLANK_SHA
D
Douwe Maan 已提交
214 215 216 217 218
        self.errors.add(:sha, " cant be 00000000 (branch removal)")
      end
    end

    def git_author_name
219
      commit.try(:author_name)
D
Douwe Maan 已提交
220 221 222
    end

    def git_author_email
223
      commit.try(:author_email)
D
Douwe Maan 已提交
224 225 226
    end

    def git_commit_message
227
      commit.try(:message)
D
Douwe Maan 已提交
228 229
    end

230 231 232 233
    def git_commit_title
      commit.try(:title)
    end

D
Douwe Maan 已提交
234
    def short_sha
235
      Ci::Pipeline.truncate_sha(sha)
D
Douwe Maan 已提交
236 237
    end

238
    def commit
K
Kamil Trzcinski 已提交
239
      @commit ||= project.commit(sha)
D
Douwe Maan 已提交
240 241 242 243
    rescue
      nil
    end

K
Kamil Trzcinski 已提交
244 245 246 247
    def branch?
      !tag?
    end

248
    def stuck?
249
      pending_builds.any?(&:stuck?)
250 251
    end

K
Kamil Trzcinski 已提交
252
    def retryable?
253
      retryable_builds.any?
K
Kamil Trzcinski 已提交
254 255
    end

256
    def cancelable?
257
      cancelable_statuses.any?
258 259
    end

L
Lin Jen-Shin 已提交
260 261 262 263
    def auto_canceled?
      canceled? && auto_canceled_by_id?
    end

K
Kamil Trzcinski 已提交
264
    def cancel_running
265
      Gitlab::OptimisticLocking.retry_lock(cancelable_statuses) do |cancelable|
L
Lin Jen-Shin 已提交
266 267 268
        cancelable.find_each do |job|
          yield(job) if block_given?
          job.cancel
269
        end
L
Lin Jen-Shin 已提交
270
      end
K
Kamil Trzcinski 已提交
271 272
    end

273 274 275 276 277
    def auto_cancel_running(pipeline)
      update(auto_canceled_by: pipeline)

      cancel_running do |job|
        job.auto_canceled_by = pipeline
278
      end
K
Kamil Trzcinski 已提交
279 280
    end

281
    def retry_failed(current_user)
D
Douwe Maan 已提交
282 283
      Ci::RetryPipelineService.new(project, current_user)
        .execute(self)
K
Kamil Trzcinski 已提交
284 285
    end

286
    def mark_as_processable_after_stage(stage_idx)
287
      builds.skipped.after_stage(stage_idx).find_each(&:process)
288 289
    end

K
Kamil Trzcinski 已提交
290 291 292 293 294 295 296
    def latest?
      return false unless ref
      commit = project.commit(ref)
      return false unless commit
      commit.sha == sha
    end

K
Kamil Trzcinski 已提交
297 298
    def retried
      @retried ||= (statuses.order(id: :desc) - statuses.latest)
D
Douwe Maan 已提交
299 300 301
    end

    def coverage
302
      coverage_array = statuses.latest.map(&:coverage).compact
K
Kamil Trzcinski 已提交
303 304
      if coverage_array.size >= 1
        '%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
D
Douwe Maan 已提交
305 306 307
      end
    end

308
    def stage_seeds
309 310
      return [] unless config_processor

311
      @stage_seeds ||= config_processor.stage_seeds(self)
312 313
    end

314 315
    def has_kubernetes_active?
      project.kubernetes_service&.active?
316 317
    end

318 319
    def has_stage_seeds?
      stage_seeds.any?
320 321
    end

C
Connor Shea 已提交
322
    def has_warnings?
323
      builds.latest.failed_but_allowed.any?
324 325
    end

326
    def set_config_source
327 328 329 330 331
      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 已提交
332 333
    end

D
Douwe Maan 已提交
334
    def config_processor
335
      return unless ci_yaml_file
336 337 338
      return @config_processor if defined?(@config_processor)

      @config_processor ||= begin
339
        Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.full_path)
340
      rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
341
        self.yaml_errors = e.message
342 343
        nil
      rescue
344
        self.yaml_errors = 'Undefined error'
345 346
        nil
      end
D
Douwe Maan 已提交
347 348
    end

349
    def ci_yaml_file_path
350
      if project.ci_config_path.blank?
351 352
        '.gitlab-ci.yml'
      else
353
        project.ci_config_path
354 355 356
      end
    end

K
Kamil Trzcinski 已提交
357
    def ci_yaml_file
358 359
      return @ci_yaml_file if defined?(@ci_yaml_file)

Z
Zeger-Jan van de Weg 已提交
360
      @ci_yaml_file =
361
        if auto_devops_source?
Z
Zeger-Jan van de Weg 已提交
362
          implied_ci_yaml_file
363 364
        else
          ci_yaml_from_repo
Z
Zeger-Jan van de Weg 已提交
365
        end
366 367 368 369 370 371

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

375 376 377 378
    def has_yaml_errors?
      yaml_errors.present?
    end

K
Kamil Trzcinski 已提交
379 380 381 382
    def environments
      builds.where.not(environment: nil).success.pluck(:environment).uniq
    end

J
James Lopez 已提交
383 384 385 386 387 388 389 390 391 392 393 394 395
    # 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

396 397 398 399
    def notes
      Note.for_commit_id(sha)
    end

400 401 402
    def process!
      Ci::ProcessPipelineService.new(project, user).execute(self)
    end
403

404
    def update_status
K
Kamil Trzcinski 已提交
405
      Gitlab::OptimisticLocking.retry_lock(self) do
K
Kamil Trzcinski 已提交
406
        case latest_builds_status
407 408 409 410 411 412
        when 'pending' then enqueue
        when 'running' then run
        when 'success' then succeed
        when 'failed' then drop
        when 'canceled' then cancel
        when 'skipped' then skip
413
        when 'manual' then block
414
        end
415
      end
416 417
    end

418 419
    def predefined_variables
      [
L
Lin Jen-Shin 已提交
420
        { key: 'CI_PIPELINE_ID', value: id.to_s, public: true },
421 422
        { key: 'CI_CONFIG_PATH', value: ci_yaml_file_path, public: true },
        { key: 'CI_PIPELINE_SOURCE', value: source.to_s, public: true }
423 424 425
      ]
    end

426 427 428 429 430 431 432
    def queued_duration
      return unless started_at

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

433
    def update_duration
434 435
      return unless started_at

436
      self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
437 438 439
    end

    def execute_hooks
K
Kamil Trzcinski 已提交
440 441 442
      data = pipeline_data
      project.execute_hooks(data, :pipeline_hooks)
      project.execute_services(data, :pipeline_hooks)
443 444
    end

445 446
    # All the merge requests for which the current pipeline runs/ran against
    def all_merge_requests
447
      @all_merge_requests ||= project.merge_requests.where(source_branch: ref)
448 449
    end

450
    def detailed_status(current_user)
D
Douwe Maan 已提交
451 452 453
      Gitlab::Ci::Status::Pipeline::Factory
        .new(self, current_user)
        .fabricate!
454 455
    end

456 457
    private

458
    def ci_yaml_from_repo
459 460 461
      return unless project
      return unless sha

462 463 464 465 466
      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 已提交
467
    def implied_ci_yaml_file
468 469
      return unless project

Z
Zeger-Jan van de Weg 已提交
470 471 472 473 474
      if project.auto_devops_enabled?
        Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content
      end
    end

475
    def pipeline_data
476
      Gitlab::DataBuilder::Pipeline.build(self)
K
Kamil Trzcinski 已提交
477
    end
478

479
    def latest_builds_status
480 481 482
      return 'failed' unless yaml_errors.blank?

      statuses.latest.status || 'skipped'
K
Kamil Trzcinski 已提交
483
    end
484 485

    def keep_around_commits
486
      return unless project
487

488 489 490
      project.repository.keep_around(self.sha)
      project.repository.keep_around(self.before_sha)
    end
D
Douwe Maan 已提交
491 492
  end
end