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

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

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

D
Douwe Maan 已提交
20 21
    delegate :id, to: :project, prefix: true

D
Douwe Maan 已提交
22 23 24
    validates :sha, presence: { unless: :importing? }
    validates :ref, presence: { unless: :importing? }
    validates :status, presence: { unless: :importing? }
25
    validate :valid_commit_sha, unless: :importing?
D
Douwe Maan 已提交
26

27
    after_create :keep_around_commits, unless: :importing?
28
    after_create :refresh_build_status_cache
K
Kamil Trzcinski 已提交
29

30
    state_machine :status, initial: :created do
31
      event :enqueue do
K
Kamil Trzcinski 已提交
32
        transition created: :pending
33
        transition [:success, :failed, :canceled, :skipped] => :running
34 35 36
      end

      event :run do
K
Kamil Trzcinski 已提交
37
        transition any - [:running] => :running
38 39
      end

40
      event :skip do
K
Kamil Trzcinski 已提交
41
        transition any - [:skipped] => :skipped
42 43 44
      end

      event :drop do
K
Kamil Trzcinski 已提交
45
        transition any - [:failed] => :failed
46 47
      end

48
      event :succeed do
K
Kamil Trzcinski 已提交
49
        transition any - [:success] => :success
50 51 52
      end

      event :cancel do
K
Kamil Trzcinski 已提交
53
        transition any - [:canceled] => :canceled
54 55
      end

56
      event :block do
57
        transition any - [:manual] => :manual
58 59
      end

K
Kamil Trzcinski 已提交
60 61 62 63
      # IMPORTANT
      # Do not add any operations to this state_machine
      # Create a separate worker for each new operation

64
      before_transition [:created, :pending] => :running do |pipeline|
65
        pipeline.started_at = Time.now
66 67
      end

68
      before_transition any => [:success, :failed, :canceled] do |pipeline|
69
        pipeline.finished_at = Time.now
70 71 72
        pipeline.update_duration
      end

L
Lin Jen-Shin 已提交
73 74 75 76
      before_transition :canceled => any - [:canceled] do |pipeline|
        pipeline.auto_canceled_by = nil
      end

77
      after_transition [:created, :pending] => :running do |pipeline|
K
Kamil Trzcinski 已提交
78
        pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) }
79 80 81
      end

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

85
      after_transition [:created, :pending, :running] => :success do |pipeline|
86
        pipeline.run_after_commit { PipelineSuccessWorker.perform_async(id) }
87
      end
88 89

      after_transition do |pipeline, transition|
90 91 92 93 94
        next if transition.loopback?

        pipeline.run_after_commit do
          PipelineHooksWorker.perform_async(id)
        end
95
      end
96

97
      after_transition any => [:success, :failed] do |pipeline|
98
        pipeline.run_after_commit do
99
          PipelineNotificationWorker.perform_async(pipeline.id)
100
        end
101
      end
102 103
    end

104
    # ref can't be HEAD or SHA, can only be branch/tag name
105
    scope :latest, ->(ref = nil) do
D
Douwe Maan 已提交
106 107 108
      max_id = unscope(:select)
        .select("max(#{quoted_table_name}.id)")
        .group(:ref, :sha)
109

110 111 112 113 114
      if ref
        where(ref: ref, id: max_id.where(ref: ref))
      else
        where(id: max_id)
      end
115
    end
116

117 118 119 120
    def self.latest_status(ref = nil)
      latest(ref).status
    end

121
    def self.latest_successful_for(ref)
122
      success.latest(ref).order(id: :desc).first
123 124
    end

125 126 127 128 129 130
    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 已提交
131 132 133 134
    def self.truncate_sha(sha)
      sha[0...8]
    end

135
    def self.total_duration
L
Lin Jen-Shin 已提交
136
      where.not(duration: nil).sum(:duration)
137 138
    end

K
Kamil Trzcinski 已提交
139 140 141 142 143
    def stage(name)
      stage = Ci::Stage.new(self, name: name)
      stage unless stage.statuses_count.zero?
    end

144 145
    def stages_count
      statuses.select(:stage).distinct.count
K
Kamil Trzcinski 已提交
146 147
    end

K
Kamil Trzcinski 已提交
148
    def stages_name
149 150
      statuses.order(:stage_idx).distinct.
        pluck(:stage, :stage_idx).map(&:first)
K
Kamil Trzcinski 已提交
151 152
    end

153
    def stages
154 155
      # TODO, this needs refactoring, see gitlab-ce#26481.

D
Douwe Maan 已提交
156 157
      stages_query = statuses
        .group('stage').select(:stage).order('max(stage_idx)')
158

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

161
      warnings_sql = statuses.latest.select('COUNT(*)')
D
Douwe Maan 已提交
162
        .where('stage=sg.stage').failed_but_allowed.to_sql
163

D
Douwe Maan 已提交
164 165
      stages_with_statuses = CommitStatus.from(stages_query, :sg)
        .pluck('sg.stage', status_sql, "(#{warnings_sql})")
K
Kamil Trzcinski 已提交
166 167

      stages_with_statuses.map do |stage|
168
        Ci::Stage.new(self, Hash[%i[name status warnings].zip(stage)])
K
Kamil Trzcinski 已提交
169 170 171 172
      end
    end

    def artifacts
173
      builds.latest.with_artifacts_not_expired.includes(project: [:namespace])
K
Kamil Trzcinski 已提交
174 175
    end

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

    def git_author_name
183
      commit.try(:author_name)
D
Douwe Maan 已提交
184 185 186
    end

    def git_author_email
187
      commit.try(:author_email)
D
Douwe Maan 已提交
188 189 190
    end

    def git_commit_message
191
      commit.try(:message)
D
Douwe Maan 已提交
192 193
    end

194 195 196 197
    def git_commit_title
      commit.try(:title)
    end

D
Douwe Maan 已提交
198
    def short_sha
199
      Ci::Pipeline.truncate_sha(sha)
D
Douwe Maan 已提交
200 201
    end

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

K
Kamil Trzcinski 已提交
208 209 210 211
    def branch?
      !tag?
    end

212
    def manual_actions
213 214 215 216
      builds.latest.manual_actions.includes(project: [:namespace])
    end

    def stuck?
217
      builds.pending.includes(:project).any?(&:stuck?)
218 219
    end

K
Kamil Trzcinski 已提交
220
    def retryable?
221
      builds.latest.failed_or_canceled.any?(&:retryable?)
K
Kamil Trzcinski 已提交
222 223
    end

224
    def cancelable?
225
      statuses.cancelable.any?
226 227
    end

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

K
Kamil Trzcinski 已提交
232
    def cancel_running
233 234
      Gitlab::OptimisticLocking.retry_lock(
        statuses.cancelable) do |cancelable|
235 236 237 238
          cancelable.find_each do |job|
            yield(job) if block_given?
            job.cancel
          end
239
        end
K
Kamil Trzcinski 已提交
240 241
    end

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

      cancel_running do |job|
        job.auto_canceled_by = pipeline
      end
    end

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

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

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

K
Kamil Trzcinski 已提交
266 267 268 269
    def triggered?
      trigger_requests.any?
    end

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

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

281 282 283
    def config_builds_attributes
      return [] unless config_processor

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

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

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

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

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

314 315 316 317
    def has_yaml_errors?
      yaml_errors.present?
    end

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

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

335 336 337 338
    def notes
      Note.for_commit_id(sha)
    end

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

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

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

364 365 366 367 368 369 370
    def queued_duration
      return unless started_at

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

371
    def update_duration
372 373
      return unless started_at

374
      self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
375 376 377
    end

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

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

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

397 398 399 400
    def refresh_build_status_cache
      Ci::PipelineStatus.new(project, sha: sha, status: status).store_in_cache_if_needed
    end

401 402
    private

403
    def pipeline_data
404
      Gitlab::DataBuilder::Pipeline.build(self)
K
Kamil Trzcinski 已提交
405
    end
406

407
    def latest_builds_status
408 409 410
      return 'failed' unless yaml_errors.blank?

      statuses.latest.status || 'skipped'
K
Kamil Trzcinski 已提交
411
    end
412 413

    def keep_around_commits
414
      return unless project
415

416 417 418
      project.repository.keep_around(self.sha)
      project.repository.keep_around(self.before_sha)
    end
D
Douwe Maan 已提交
419 420
  end
end