pipeline.rb 11.1 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 :auto_canceled_pipelines, class_name: 'Ci::Pipeline', foreign_key: 'auto_canceled_by_id'
15
    has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id'
16

17
    has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
18 19
    has_many :builds, foreign_key: :commit_id
    has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id
20
    has_many :merge_requests, foreign_key: "head_pipeline_id"
D
Douwe Maan 已提交
21

22 23 24 25 26 27
    has_many :pending_builds, -> { pending }, foreign_key: :commit_id, class_name: 'Ci::Build'
    has_many :retryable_builds, -> { latest.failed_or_canceled }, foreign_key: :commit_id, class_name: 'Ci::Build'
    has_many :cancelable_statuses, -> { cancelable }, foreign_key: :commit_id, class_name: 'CommitStatus'
    has_many :manual_actions, -> { latest.manual_actions }, foreign_key: :commit_id, class_name: 'Ci::Build'
    has_many :artifacts, -> { latest.with_artifacts_not_expired }, foreign_key: :commit_id, class_name: 'Ci::Build'

D
Douwe Maan 已提交
28 29
    delegate :id, to: :project, prefix: true

D
Douwe Maan 已提交
30 31 32
    validates :sha, presence: { unless: :importing? }
    validates :ref, presence: { unless: :importing? }
    validates :status, presence: { unless: :importing? }
33
    validate :valid_commit_sha, unless: :importing?
D
Douwe Maan 已提交
34

35
    after_create :keep_around_commits, unless: :importing?
K
Kamil Trzcinski 已提交
36

37
    state_machine :status, initial: :created do
38
      event :enqueue do
K
Kamil Trzcinski 已提交
39
        transition created: :pending
40
        transition [:success, :failed, :canceled, :skipped] => :running
41 42 43
      end

      event :run do
K
Kamil Trzcinski 已提交
44
        transition any - [:running] => :running
45 46
      end

47
      event :skip do
K
Kamil Trzcinski 已提交
48
        transition any - [:skipped] => :skipped
49 50 51
      end

      event :drop do
K
Kamil Trzcinski 已提交
52
        transition any - [:failed] => :failed
53 54
      end

55
      event :succeed do
K
Kamil Trzcinski 已提交
56
        transition any - [:success] => :success
57 58 59
      end

      event :cancel do
K
Kamil Trzcinski 已提交
60
        transition any - [:canceled] => :canceled
61 62
      end

63
      event :block do
64
        transition any - [:manual] => :manual
65 66
      end

K
Kamil Trzcinski 已提交
67 68 69 70
      # IMPORTANT
      # Do not add any operations to this state_machine
      # Create a separate worker for each new operation

71
      before_transition [:created, :pending] => :running do |pipeline|
72
        pipeline.started_at = Time.now
73 74
      end

75
      before_transition any => [:success, :failed, :canceled] do |pipeline|
76
        pipeline.finished_at = Time.now
77 78 79
        pipeline.update_duration
      end

80 81 82 83
      before_transition any => [:manual] do |pipeline|
        pipeline.update_duration
      end

L
Lin Jen-Shin 已提交
84
      before_transition canceled: any - [:canceled] do |pipeline|
L
Lin Jen-Shin 已提交
85 86 87
        pipeline.auto_canceled_by = nil
      end

88
      after_transition [:created, :pending] => :running do |pipeline|
89
        pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
90 91 92
      end

      after_transition any => [:success] do |pipeline|
93
        pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
94 95
      end

96
      after_transition [:created, :pending, :running] => :success do |pipeline|
97
        pipeline.run_after_commit { PipelineSuccessWorker.perform_async(pipeline.id) }
98
      end
99 100

      after_transition do |pipeline, transition|
101 102 103
        next if transition.loopback?

        pipeline.run_after_commit do
104
          PipelineHooksWorker.perform_async(pipeline.id)
105
          ExpirePipelineCacheWorker.perform_async(pipeline.id)
106
        end
107
      end
108

109
      after_transition any => [:success, :failed] do |pipeline|
110
        pipeline.run_after_commit do
111
          PipelineNotificationWorker.perform_async(pipeline.id)
112
        end
113
      end
114 115
    end

116
    # ref can't be HEAD or SHA, can only be branch/tag name
117
    scope :latest, ->(ref = nil) do
D
Douwe Maan 已提交
118 119 120
      max_id = unscope(:select)
        .select("max(#{quoted_table_name}.id)")
        .group(:ref, :sha)
121

122 123 124 125 126
      if ref
        where(ref: ref, id: max_id.where(ref: ref))
      else
        where(id: max_id)
      end
127
    end
128

129 130 131 132
    def self.latest_status(ref = nil)
      latest(ref).status
    end

133
    def self.latest_successful_for(ref)
134
      success.latest(ref).order(id: :desc).first
135 136
    end

137 138 139 140 141 142
    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 已提交
143 144 145 146
    def self.truncate_sha(sha)
      sha[0...8]
    end

147
    def self.total_duration
L
Lin Jen-Shin 已提交
148
      where.not(duration: nil).sum(:duration)
149 150
    end

K
Kamil Trzcinski 已提交
151 152 153 154 155
    def stage(name)
      stage = Ci::Stage.new(self, name: name)
      stage unless stage.statuses_count.zero?
    end

156 157
    def stages_count
      statuses.select(:stage).distinct.count
K
Kamil Trzcinski 已提交
158 159
    end

K
Kamil Trzcinski 已提交
160
    def stages_name
161 162
      statuses.order(:stage_idx).distinct.
        pluck(:stage, :stage_idx).map(&:first)
K
Kamil Trzcinski 已提交
163 164
    end

165
    def stages
166 167
      # TODO, this needs refactoring, see gitlab-ce#26481.

D
Douwe Maan 已提交
168 169
      stages_query = statuses
        .group('stage').select(:stage).order('max(stage_idx)')
170

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

173
      warnings_sql = statuses.latest.select('COUNT(*)')
D
Douwe Maan 已提交
174
        .where('stage=sg.stage').failed_but_allowed.to_sql
175

D
Douwe Maan 已提交
176 177
      stages_with_statuses = CommitStatus.from(stages_query, :sg)
        .pluck('sg.stage', status_sql, "(#{warnings_sql})")
K
Kamil Trzcinski 已提交
178 179

      stages_with_statuses.map do |stage|
180
        Ci::Stage.new(self, Hash[%i[name status warnings].zip(stage)])
K
Kamil Trzcinski 已提交
181 182 183
      end
    end

D
Douwe Maan 已提交
184
    def valid_commit_sha
185
      if self.sha == Gitlab::Git::BLANK_SHA
D
Douwe Maan 已提交
186 187 188 189 190
        self.errors.add(:sha, " cant be 00000000 (branch removal)")
      end
    end

    def git_author_name
191
      commit.try(:author_name)
D
Douwe Maan 已提交
192 193 194
    end

    def git_author_email
195
      commit.try(:author_email)
D
Douwe Maan 已提交
196 197 198
    end

    def git_commit_message
199
      commit.try(:message)
D
Douwe Maan 已提交
200 201
    end

202 203 204 205
    def git_commit_title
      commit.try(:title)
    end

D
Douwe Maan 已提交
206
    def short_sha
207
      Ci::Pipeline.truncate_sha(sha)
D
Douwe Maan 已提交
208 209
    end

210
    def commit
K
Kamil Trzcinski 已提交
211
      @commit ||= project.commit(sha)
D
Douwe Maan 已提交
212 213 214 215
    rescue
      nil
    end

K
Kamil Trzcinski 已提交
216 217 218 219
    def branch?
      !tag?
    end

220
    def stuck?
221
      pending_builds.any?(&:stuck?)
222 223
    end

K
Kamil Trzcinski 已提交
224
    def retryable?
225
      retryable_builds.any?
K
Kamil Trzcinski 已提交
226 227
    end

228
    def cancelable?
229
      cancelable_statuses.any?
230 231
    end

L
Lin Jen-Shin 已提交
232 233 234 235
    def auto_canceled?
      canceled? && auto_canceled_by_id?
    end

K
Kamil Trzcinski 已提交
236
    def cancel_running
237
      Gitlab::OptimisticLocking.retry_lock(cancelable_statuses) do |cancelable|
L
Lin Jen-Shin 已提交
238 239 240
        cancelable.find_each do |job|
          yield(job) if block_given?
          job.cancel
241
        end
L
Lin Jen-Shin 已提交
242
      end
K
Kamil Trzcinski 已提交
243 244
    end

245 246 247 248 249
    def auto_cancel_running(pipeline)
      update(auto_canceled_by: pipeline)

      cancel_running do |job|
        job.auto_canceled_by = pipeline
250
      end
K
Kamil Trzcinski 已提交
251 252
    end

253
    def retry_failed(current_user)
D
Douwe Maan 已提交
254 255
      Ci::RetryPipelineService.new(project, current_user)
        .execute(self)
K
Kamil Trzcinski 已提交
256 257
    end

258
    def mark_as_processable_after_stage(stage_idx)
259
      builds.skipped.after_stage(stage_idx).find_each(&:process)
260 261
    end

K
Kamil Trzcinski 已提交
262 263 264 265 266 267 268
    def latest?
      return false unless ref
      commit = project.commit(ref)
      return false unless commit
      commit.sha == sha
    end

K
Kamil Trzcinski 已提交
269 270 271 272
    def triggered?
      trigger_requests.any?
    end

K
Kamil Trzcinski 已提交
273 274
    def retried
      @retried ||= (statuses.order(id: :desc) - statuses.latest)
D
Douwe Maan 已提交
275 276 277
    end

    def coverage
278
      coverage_array = statuses.latest.map(&:coverage).compact
K
Kamil Trzcinski 已提交
279 280
      if coverage_array.size >= 1
        '%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
D
Douwe Maan 已提交
281 282 283
      end
    end

284 285 286
    def config_builds_attributes
      return [] unless config_processor

287 288 289
      config_processor.
        builds_for_ref(ref, tag?, trigger_requests.first).
        sort_by { |build| build[:stage_idx] }
290 291
    end

C
Connor Shea 已提交
292
    def has_warnings?
293
      builds.latest.failed_but_allowed.any?
294 295
    end

D
Douwe Maan 已提交
296
    def config_processor
297
      return nil unless ci_yaml_file
298 299 300 301 302
      return @config_processor if defined?(@config_processor)

      @config_processor ||= begin
        Ci::GitlabCiYamlProcessor.new(ci_yaml_file, project.path_with_namespace)
      rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
303
        self.yaml_errors = e.message
304 305
        nil
      rescue
306
        self.yaml_errors = 'Undefined error'
307 308
        nil
      end
D
Douwe Maan 已提交
309 310
    end

K
Kamil Trzcinski 已提交
311
    def ci_yaml_file
312 313
      return @ci_yaml_file if defined?(@ci_yaml_file)

D
Douwe Maan 已提交
314
      @ci_yaml_file = project.repository.gitlab_ci_yml_for(sha) rescue nil
K
Kamil Trzcinski 已提交
315 316
    end

317 318 319 320
    def has_yaml_errors?
      yaml_errors.present?
    end

K
Kamil Trzcinski 已提交
321 322 323 324
    def environments
      builds.where.not(environment: nil).success.pluck(:environment).uniq
    end

J
James Lopez 已提交
325 326 327 328 329 330 331 332 333 334 335 336 337
    # 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

338 339 340 341
    def notes
      Note.for_commit_id(sha)
    end

342 343 344
    def process!
      Ci::ProcessPipelineService.new(project, user).execute(self)
    end
345

346
    def update_status
K
Kamil Trzcinski 已提交
347
      Gitlab::OptimisticLocking.retry_lock(self) do
K
Kamil Trzcinski 已提交
348
        case latest_builds_status
349 350 351 352 353 354
        when 'pending' then enqueue
        when 'running' then run
        when 'success' then succeed
        when 'failed' then drop
        when 'canceled' then cancel
        when 'skipped' then skip
355
        when 'manual' then block
356
        end
357
      end
358 359
    end

360 361 362 363 364 365
    def predefined_variables
      [
        { key: 'CI_PIPELINE_ID', value: id.to_s, public: true }
      ]
    end

366 367 368 369 370 371 372
    def queued_duration
      return unless started_at

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

373
    def update_duration
374 375
      return unless started_at

376
      self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
377 378 379
    end

    def execute_hooks
K
Kamil Trzcinski 已提交
380 381 382
      data = pipeline_data
      project.execute_hooks(data, :pipeline_hooks)
      project.execute_services(data, :pipeline_hooks)
383 384
    end

385 386 387
    # Merge requests for which the current pipeline is running against
    # the merge request's latest commit.
    def merge_requests
D
Douwe Maan 已提交
388 389 390
      @merge_requests ||= project.merge_requests
        .where(source_branch: self.ref)
        .select { |merge_request| merge_request.head_pipeline.try(:id) == self.id }
391 392
    end

393 394
    # All the merge requests for which the current pipeline runs/ran against
    def all_merge_requests
395
      @all_merge_requests ||= project.merge_requests.where(source_branch: ref)
396 397
    end

398
    def detailed_status(current_user)
D
Douwe Maan 已提交
399 400 401
      Gitlab::Ci::Status::Pipeline::Factory
        .new(self, current_user)
        .fabricate!
402 403
    end

404 405
    private

406
    def pipeline_data
407
      Gitlab::DataBuilder::Pipeline.build(self)
K
Kamil Trzcinski 已提交
408
    end
409

410
    def latest_builds_status
411 412 413
      return 'failed' unless yaml_errors.blank?

      statuses.latest.status || 'skipped'
K
Kamil Trzcinski 已提交
414
    end
415 416

    def keep_around_commits
417
      return unless project
418

419 420 421
      project.repository.keep_around(self.sha)
      project.repository.keep_around(self.before_sha)
    end
D
Douwe Maan 已提交
422 423
  end
end