pipeline.rb 12.2 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 54 55 56 57
    enum config_source: {
      repository: nil,
      auto_devops: 1
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

306
    def stage_seeds
307 308
      return [] unless config_processor

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

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

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

D
Douwe Maan 已提交
320
    def config_processor
321
      return unless ci_yaml_file
322 323 324
      return @config_processor if defined?(@config_processor)

      @config_processor ||= begin
325
        Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.full_path)
326
      rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
327
        self.yaml_errors = e.message
328 329
        nil
      rescue
330
        self.yaml_errors = 'Undefined error'
331 332
        nil
      end
D
Douwe Maan 已提交
333 334
    end

335
    def ci_yaml_file_path
336
      if project.ci_config_path.blank?
337 338
        '.gitlab-ci.yml'
      else
339
        project.ci_config_path
340 341 342
      end
    end

K
Kamil Trzcinski 已提交
343
    def ci_yaml_file
344 345
      return @ci_yaml_file if defined?(@ci_yaml_file)

346 347 348 349 350 351 352 353
      @ci_yaml_file = ci_yaml_from_repo
      @ci_yaml_file ||= implied_ci_yaml_file&.tap { self.auto_devops! }

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

357 358 359 360
    def has_yaml_errors?
      yaml_errors.present?
    end

K
Kamil Trzcinski 已提交
361 362 363 364
    def environments
      builds.where.not(environment: nil).success.pluck(:environment).uniq
    end

J
James Lopez 已提交
365 366 367 368 369 370 371 372 373 374 375 376 377
    # 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

378 379 380 381
    def notes
      Note.for_commit_id(sha)
    end

382 383 384
    def process!
      Ci::ProcessPipelineService.new(project, user).execute(self)
    end
385

386
    def update_status
K
Kamil Trzcinski 已提交
387
      Gitlab::OptimisticLocking.retry_lock(self) do
K
Kamil Trzcinski 已提交
388
        case latest_builds_status
389 390 391 392 393 394
        when 'pending' then enqueue
        when 'running' then run
        when 'success' then succeed
        when 'failed' then drop
        when 'canceled' then cancel
        when 'skipped' then skip
395
        when 'manual' then block
396
        end
397
      end
398 399
    end

400 401
    def predefined_variables
      [
L
Lin Jen-Shin 已提交
402
        { key: 'CI_PIPELINE_ID', value: id.to_s, public: true },
403 404
        { key: 'CI_CONFIG_PATH', value: ci_yaml_file_path, public: true },
        { key: 'CI_PIPELINE_SOURCE', value: source.to_s, public: true }
405 406 407
      ]
    end

408 409 410 411 412 413 414
    def queued_duration
      return unless started_at

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

415
    def update_duration
416 417
      return unless started_at

418
      self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
419 420 421
    end

    def execute_hooks
K
Kamil Trzcinski 已提交
422 423 424
      data = pipeline_data
      project.execute_hooks(data, :pipeline_hooks)
      project.execute_services(data, :pipeline_hooks)
425 426
    end

427 428
    # All the merge requests for which the current pipeline runs/ran against
    def all_merge_requests
429
      @all_merge_requests ||= project.merge_requests.where(source_branch: ref)
430 431
    end

432
    def detailed_status(current_user)
D
Douwe Maan 已提交
433 434 435
      Gitlab::Ci::Status::Pipeline::Factory
        .new(self, current_user)
        .fabricate!
436 437
    end

438 439
    private

440 441 442 443 444 445 446 447 448 449 450 451
    def implied_ci_yaml_file
      if project.auto_devops_enabled?
        Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content
      end
    end

    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

452
    def pipeline_data
453
      Gitlab::DataBuilder::Pipeline.build(self)
K
Kamil Trzcinski 已提交
454
    end
455

456
    def latest_builds_status
457 458 459
      return 'failed' unless yaml_errors.blank?

      statuses.latest.status || 'skipped'
K
Kamil Trzcinski 已提交
460
    end
461 462

    def keep_around_commits
463
      return unless project
464

465 466 467
      project.repository.keep_around(self.sha)
      project.repository.keep_around(self.before_sha)
    end
D
Douwe Maan 已提交
468 469
  end
end