pipeline.rb 12.5 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_create :keep_around_commits, unless: :importing?
K
Kamil Trzcinski 已提交
42

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

307
    def stage_seeds
308 309
      return [] unless config_processor

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

313 314
    def has_stage_seeds?
      stage_seeds.any?
315 316
    end

C
Connor Shea 已提交
317
    def has_warnings?
318
      builds.latest.failed_but_allowed.any?
319 320
    end

Z
Zeger-Jan van de Weg 已提交
321 322 323 324 325
    def detect_ci_yaml_file
      ci_yaml_from_repo&.tap { self.repository_source! } ||
        implied_ci_yaml_file&.tap { self.auto_devops_source! }
    end

D
Douwe Maan 已提交
326
    def config_processor
327
      return unless ci_yaml_file
328 329 330
      return @config_processor if defined?(@config_processor)

      @config_processor ||= begin
331
        Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.full_path)
332
      rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
333
        self.yaml_errors = e.message
334 335
        nil
      rescue
336
        self.yaml_errors = 'Undefined error'
337 338
        nil
      end
D
Douwe Maan 已提交
339 340
    end

341
    def ci_yaml_file_path
342
      if project.ci_config_path.blank?
343 344
        '.gitlab-ci.yml'
      else
345
        project.ci_config_path
346 347 348
      end
    end

K
Kamil Trzcinski 已提交
349
    def ci_yaml_file
350 351
      return @ci_yaml_file if defined?(@ci_yaml_file)

Z
Zeger-Jan van de Weg 已提交
352 353
      @ci_yaml_file =
        case config_source
354
        when "repository_source", "unknown_source"
Z
Zeger-Jan van de Weg 已提交
355
          ci_yaml_from_repo
356
        when "auto_devops_source"
Z
Zeger-Jan van de Weg 已提交
357 358
          implied_ci_yaml_file
        end
359 360 361 362 363 364

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

368 369 370 371
    def has_yaml_errors?
      yaml_errors.present?
    end

K
Kamil Trzcinski 已提交
372 373 374 375
    def environments
      builds.where.not(environment: nil).success.pluck(:environment).uniq
    end

J
James Lopez 已提交
376 377 378 379 380 381 382 383 384 385 386 387 388
    # 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

389 390 391 392
    def notes
      Note.for_commit_id(sha)
    end

393 394 395
    def process!
      Ci::ProcessPipelineService.new(project, user).execute(self)
    end
396

397
    def update_status
K
Kamil Trzcinski 已提交
398
      Gitlab::OptimisticLocking.retry_lock(self) do
K
Kamil Trzcinski 已提交
399
        case latest_builds_status
400 401 402 403 404 405
        when 'pending' then enqueue
        when 'running' then run
        when 'success' then succeed
        when 'failed' then drop
        when 'canceled' then cancel
        when 'skipped' then skip
406
        when 'manual' then block
407
        end
408
      end
409 410
    end

411 412
    def predefined_variables
      [
L
Lin Jen-Shin 已提交
413
        { key: 'CI_PIPELINE_ID', value: id.to_s, public: true },
414 415
        { key: 'CI_CONFIG_PATH', value: ci_yaml_file_path, public: true },
        { key: 'CI_PIPELINE_SOURCE', value: source.to_s, public: true }
416 417 418
      ]
    end

419 420 421 422 423 424 425
    def queued_duration
      return unless started_at

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

426
    def update_duration
427 428
      return unless started_at

429
      self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
430 431 432
    end

    def execute_hooks
K
Kamil Trzcinski 已提交
433 434 435
      data = pipeline_data
      project.execute_hooks(data, :pipeline_hooks)
      project.execute_services(data, :pipeline_hooks)
436 437
    end

438 439
    # All the merge requests for which the current pipeline runs/ran against
    def all_merge_requests
440
      @all_merge_requests ||= project.merge_requests.where(source_branch: ref)
441 442
    end

443
    def detailed_status(current_user)
D
Douwe Maan 已提交
444 445 446
      Gitlab::Ci::Status::Pipeline::Factory
        .new(self, current_user)
        .fabricate!
447 448
    end

449 450
    private

451 452 453 454 455 456
    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 已提交
457 458 459 460 461 462
    def implied_ci_yaml_file
      if project.auto_devops_enabled?
        Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content
      end
    end

463
    def pipeline_data
464
      Gitlab::DataBuilder::Pipeline.build(self)
K
Kamil Trzcinski 已提交
465
    end
466

467
    def latest_builds_status
468 469 470
      return 'failed' unless yaml_errors.blank?

      statuses.latest.status || 'skipped'
K
Kamil Trzcinski 已提交
471
    end
472 473

    def keep_around_commits
474
      return unless project
475

476 477 478
      project.repository.keep_around(self.sha)
      project.repository.keep_around(self.before_sha)
    end
D
Douwe Maan 已提交
479 480
  end
end