pipeline.rb 12.8 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? }
S
Shinya Maeda 已提交
39
    validates :protected, inclusion: { in: [true, false], unless: :importing? }, on: :create
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
    state_machine :status, initial: :created do
62
      event :enqueue do
K
Kamil Trzcinski 已提交
63
        transition created: :pending
64
        transition [:success, :failed, :canceled, :skipped] => :running
65 66 67
      end

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

309
    def stage_seeds
310 311
      return [] unless config_processor

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

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

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

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

327 328 329 330 331 332 333 334 335 336
    def set_config_source
      self.config_source =
        if project
          case
          when ci_yaml_from_repo then :repository_source
          when implied_ci_yaml_file then :auto_devops_source
          end
        else
          :unknown_source
        end
Z
Zeger-Jan van de Weg 已提交
337 338
    end

D
Douwe Maan 已提交
339
    def config_processor
340
      return unless ci_yaml_file
341 342 343
      return @config_processor if defined?(@config_processor)

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

354
    def ci_yaml_file_path
355
      if project.ci_config_path.blank?
356 357
        '.gitlab-ci.yml'
      else
358
        project.ci_config_path
359 360 361
      end
    end

K
Kamil Trzcinski 已提交
362
    def ci_yaml_file
363 364
      return @ci_yaml_file if defined?(@ci_yaml_file)

Z
Zeger-Jan van de Weg 已提交
365
      @ci_yaml_file =
366
        if auto_devops_source?
Z
Zeger-Jan van de Weg 已提交
367
          implied_ci_yaml_file
368 369
        else
          ci_yaml_from_repo
Z
Zeger-Jan van de Weg 已提交
370
        end
371 372 373 374 375 376

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

380 381 382 383
    def has_yaml_errors?
      yaml_errors.present?
    end

K
Kamil Trzcinski 已提交
384 385 386 387
    def environments
      builds.where.not(environment: nil).success.pluck(:environment).uniq
    end

J
James Lopez 已提交
388 389 390 391 392 393 394 395 396 397 398 399 400
    # 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

401 402 403 404
    def notes
      Note.for_commit_id(sha)
    end

405 406 407
    def process!
      Ci::ProcessPipelineService.new(project, user).execute(self)
    end
408

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

423 424
    def predefined_variables
      [
L
Lin Jen-Shin 已提交
425
        { key: 'CI_PIPELINE_ID', value: id.to_s, public: true },
426 427
        { key: 'CI_CONFIG_PATH', value: ci_yaml_file_path, public: true },
        { key: 'CI_PIPELINE_SOURCE', value: source.to_s, public: true }
428 429 430
      ]
    end

431 432 433 434 435 436 437
    def queued_duration
      return unless started_at

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

438
    def update_duration
439 440
      return unless started_at

441
      self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
442 443 444
    end

    def execute_hooks
K
Kamil Trzcinski 已提交
445 446 447
      data = pipeline_data
      project.execute_hooks(data, :pipeline_hooks)
      project.execute_services(data, :pipeline_hooks)
448 449
    end

450 451
    # All the merge requests for which the current pipeline runs/ran against
    def all_merge_requests
452
      @all_merge_requests ||= project.merge_requests.where(source_branch: ref)
453 454
    end

455
    def detailed_status(current_user)
D
Douwe Maan 已提交
456 457 458
      Gitlab::Ci::Status::Pipeline::Factory
        .new(self, current_user)
        .fabricate!
459 460
    end

461 462
    private

463 464 465 466 467 468
    def ci_yaml_from_repo
      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 已提交
469 470 471 472 473 474
    def implied_ci_yaml_file
      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