pipeline.rb 10.6 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

13
    has_many :auto_canceled_pipelines, class_name: 'Ci::Pipeline', foreign_key: 'auto_canceled_by_id'
14
    has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id'
15

16
    has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
17 18
    has_many :builds, foreign_key: :commit_id
    has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id
D
Douwe Maan 已提交
19

20 21 22 23 24 25
    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 已提交
26 27
    delegate :id, to: :project, prefix: true

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

33
    after_create :keep_around_commits, unless: :importing?
K
Kamil Trzcinski 已提交
34

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

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

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

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

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

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

61
      event :block do
62
        transition any - [:manual] => :manual
63 64
      end

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

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

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

L
Lin Jen-Shin 已提交
78
      before_transition canceled: any - [:canceled] do |pipeline|
L
Lin Jen-Shin 已提交
79 80 81
        pipeline.auto_canceled_by = nil
      end

82
      after_transition [:created, :pending] => :running do |pipeline|
K
Kamil Trzcinski 已提交
83
        pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) }
84 85 86
      end

      after_transition any => [:success] do |pipeline|
K
Kamil Trzcinski 已提交
87
        pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) }
88 89
      end

90
      after_transition [:created, :pending, :running] => :success do |pipeline|
91
        pipeline.run_after_commit { PipelineSuccessWorker.perform_async(id) }
92
      end
93 94

      after_transition do |pipeline, transition|
95 96 97 98
        next if transition.loopback?

        pipeline.run_after_commit do
          PipelineHooksWorker.perform_async(id)
T
Toon Claes 已提交
99 100
          Ci::ExpirePipelineCacheService.new(project, nil)
            .execute(pipeline)
101
        end
102
      end
103

104
      after_transition any => [:success, :failed] do |pipeline|
105
        pipeline.run_after_commit do
106
          PipelineNotificationWorker.perform_async(pipeline.id)
107
        end
108
      end
109 110
    end

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

117 118 119 120 121
      if ref
        where(ref: ref, id: max_id.where(ref: ref))
      else
        where(id: max_id)
      end
122
    end
123

124 125 126 127
    def self.latest_status(ref = nil)
      latest(ref).status
    end

128
    def self.latest_successful_for(ref)
129
      success.latest(ref).order(id: :desc).first
130 131
    end

132 133 134 135 136 137
    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 已提交
138 139 140 141
    def self.truncate_sha(sha)
      sha[0...8]
    end

142
    def self.total_duration
L
Lin Jen-Shin 已提交
143
      where.not(duration: nil).sum(:duration)
144 145
    end

K
Kamil Trzcinski 已提交
146 147 148 149 150
    def stage(name)
      stage = Ci::Stage.new(self, name: name)
      stage unless stage.statuses_count.zero?
    end

151 152
    def stages_count
      statuses.select(:stage).distinct.count
K
Kamil Trzcinski 已提交
153 154
    end

K
Kamil Trzcinski 已提交
155
    def stages_name
156 157
      statuses.order(:stage_idx).distinct.
        pluck(:stage, :stage_idx).map(&:first)
K
Kamil Trzcinski 已提交
158 159
    end

160
    def stages
161 162
      # TODO, this needs refactoring, see gitlab-ce#26481.

D
Douwe Maan 已提交
163 164
      stages_query = statuses
        .group('stage').select(:stage).order('max(stage_idx)')
165

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

168
      warnings_sql = statuses.latest.select('COUNT(*)')
D
Douwe Maan 已提交
169
        .where('stage=sg.stage').failed_but_allowed.to_sql
170

D
Douwe Maan 已提交
171 172
      stages_with_statuses = CommitStatus.from(stages_query, :sg)
        .pluck('sg.stage', status_sql, "(#{warnings_sql})")
K
Kamil Trzcinski 已提交
173 174

      stages_with_statuses.map do |stage|
175
        Ci::Stage.new(self, Hash[%i[name status warnings].zip(stage)])
K
Kamil Trzcinski 已提交
176 177 178
      end
    end

D
Douwe Maan 已提交
179
    def valid_commit_sha
180
      if self.sha == Gitlab::Git::BLANK_SHA
D
Douwe Maan 已提交
181 182 183 184 185
        self.errors.add(:sha, " cant be 00000000 (branch removal)")
      end
    end

    def git_author_name
186
      commit.try(:author_name)
D
Douwe Maan 已提交
187 188 189
    end

    def git_author_email
190
      commit.try(:author_email)
D
Douwe Maan 已提交
191 192 193
    end

    def git_commit_message
194
      commit.try(:message)
D
Douwe Maan 已提交
195 196
    end

197 198 199 200
    def git_commit_title
      commit.try(:title)
    end

D
Douwe Maan 已提交
201
    def short_sha
202
      Ci::Pipeline.truncate_sha(sha)
D
Douwe Maan 已提交
203 204
    end

205
    def commit
K
Kamil Trzcinski 已提交
206
      @commit ||= project.commit(sha)
D
Douwe Maan 已提交
207 208 209 210
    rescue
      nil
    end

K
Kamil Trzcinski 已提交
211 212 213 214
    def branch?
      !tag?
    end

215
    def stuck?
216
      pending_builds.any?(&:stuck?)
217 218
    end

K
Kamil Trzcinski 已提交
219
    def retryable?
220
      retryable_builds.any?
K
Kamil Trzcinski 已提交
221 222
    end

223
    def cancelable?
224
      cancelable_statuses.any?
225 226
    end

L
Lin Jen-Shin 已提交
227 228 229 230
    def auto_canceled?
      canceled? && auto_canceled_by_id?
    end

K
Kamil Trzcinski 已提交
231
    def cancel_running
232
      Gitlab::OptimisticLocking.retry_lock(cancelable_statuses) do |cancelable|
L
Lin Jen-Shin 已提交
233 234 235
        cancelable.find_each do |job|
          yield(job) if block_given?
          job.cancel
236
        end
L
Lin Jen-Shin 已提交
237
      end
K
Kamil Trzcinski 已提交
238 239
    end

240 241 242 243 244
    def auto_cancel_running(pipeline)
      update(auto_canceled_by: pipeline)

      cancel_running do |job|
        job.auto_canceled_by = pipeline
245
      end
K
Kamil Trzcinski 已提交
246 247
    end

248
    def retry_failed(current_user)
D
Douwe Maan 已提交
249 250
      Ci::RetryPipelineService.new(project, current_user)
        .execute(self)
K
Kamil Trzcinski 已提交
251 252
    end

253
    def mark_as_processable_after_stage(stage_idx)
254
      builds.skipped.after_stage(stage_idx).find_each(&:process)
255 256
    end

K
Kamil Trzcinski 已提交
257 258 259 260 261 262 263
    def latest?
      return false unless ref
      commit = project.commit(ref)
      return false unless commit
      commit.sha == sha
    end

K
Kamil Trzcinski 已提交
264 265 266 267
    def triggered?
      trigger_requests.any?
    end

K
Kamil Trzcinski 已提交
268 269
    def retried
      @retried ||= (statuses.order(id: :desc) - statuses.latest)
D
Douwe Maan 已提交
270 271 272
    end

    def coverage
273
      coverage_array = statuses.latest.map(&:coverage).compact
K
Kamil Trzcinski 已提交
274 275
      if coverage_array.size >= 1
        '%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
D
Douwe Maan 已提交
276 277 278
      end
    end

279 280 281
    def config_builds_attributes
      return [] unless config_processor

282 283 284
      config_processor.
        builds_for_ref(ref, tag?, trigger_requests.first).
        sort_by { |build| build[:stage_idx] }
285 286
    end

C
Connor Shea 已提交
287
    def has_warnings?
288
      builds.latest.failed_but_allowed.any?
289 290
    end

D
Douwe Maan 已提交
291
    def config_processor
292
      return nil unless ci_yaml_file
293 294 295 296 297
      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
298
        self.yaml_errors = e.message
299 300
        nil
      rescue
301
        self.yaml_errors = 'Undefined error'
302 303
        nil
      end
D
Douwe Maan 已提交
304 305
    end

K
Kamil Trzcinski 已提交
306
    def ci_yaml_file
307 308
      return @ci_yaml_file if defined?(@ci_yaml_file)

D
Douwe Maan 已提交
309
      @ci_yaml_file = project.repository.gitlab_ci_yml_for(sha) rescue nil
K
Kamil Trzcinski 已提交
310 311
    end

312 313 314 315
    def has_yaml_errors?
      yaml_errors.present?
    end

K
Kamil Trzcinski 已提交
316 317 318 319
    def environments
      builds.where.not(environment: nil).success.pluck(:environment).uniq
    end

J
James Lopez 已提交
320 321 322 323 324 325 326 327 328 329 330 331 332
    # 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

333 334 335 336
    def notes
      Note.for_commit_id(sha)
    end

337 338 339
    def process!
      Ci::ProcessPipelineService.new(project, user).execute(self)
    end
340

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

355 356 357 358 359 360
    def predefined_variables
      [
        { key: 'CI_PIPELINE_ID', value: id.to_s, public: true }
      ]
    end

361 362 363 364 365 366 367
    def queued_duration
      return unless started_at

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

368
    def update_duration
369 370
      return unless started_at

371
      self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
372 373 374
    end

    def execute_hooks
K
Kamil Trzcinski 已提交
375 376 377
      data = pipeline_data
      project.execute_hooks(data, :pipeline_hooks)
      project.execute_services(data, :pipeline_hooks)
378 379
    end

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

388
    def detailed_status(current_user)
D
Douwe Maan 已提交
389 390 391
      Gitlab::Ci::Status::Pipeline::Factory
        .new(self, current_user)
        .fabricate!
392 393
    end

394 395
    private

396
    def pipeline_data
397
      Gitlab::DataBuilder::Pipeline.build(self)
K
Kamil Trzcinski 已提交
398
    end
399

400
    def latest_builds_status
401 402 403
      return 'failed' unless yaml_errors.blank?

      statuses.latest.status || 'skipped'
K
Kamil Trzcinski 已提交
404
    end
405 406

    def keep_around_commits
407
      return unless project
408

409 410 411
      project.repository.keep_around(self.sha)
      project.repository.keep_around(self.before_sha)
    end
D
Douwe Maan 已提交
412 413
  end
end