pipeline.rb 10.0 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 :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
15 16
    has_many :builds, foreign_key: :commit_id
    has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id
D
Douwe Maan 已提交
17

D
Douwe Maan 已提交
18 19
    delegate :id, to: :project, prefix: true

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

25
    after_create :keep_around_commits, unless: :importing?
26
    after_create :refresh_build_status_cache
K
Kamil Trzcinski 已提交
27

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

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

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

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

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

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

54
      event :block do
55
        transition any - [:manual] => :manual
56 57
      end

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

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

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

71
      after_transition [:created, :pending] => :running do |pipeline|
K
Kamil Trzcinski 已提交
72
        pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) }
73 74 75
      end

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

79
      after_transition [:created, :pending, :running] => :success do |pipeline|
80
        pipeline.run_after_commit { PipelineSuccessWorker.perform_async(id) }
81
      end
82 83

      after_transition do |pipeline, transition|
84 85 86 87 88
        next if transition.loopback?

        pipeline.run_after_commit do
          PipelineHooksWorker.perform_async(id)
        end
89
      end
90

91
      after_transition any => [:success, :failed] do |pipeline|
92
        pipeline.run_after_commit do
93
          PipelineNotificationWorker.perform_async(pipeline.id)
94
        end
95
      end
96 97
    end

98
    # ref can't be HEAD or SHA, can only be branch/tag name
99
    scope :latest, ->(ref = nil) do
D
Douwe Maan 已提交
100 101 102
      max_id = unscope(:select)
        .select("max(#{quoted_table_name}.id)")
        .group(:ref, :sha)
103

104 105 106 107 108
      if ref
        where(ref: ref, id: max_id.where(ref: ref))
      else
        where(id: max_id)
      end
109
    end
110

111 112 113 114
    def self.latest_status(ref = nil)
      latest(ref).status
    end

115
    def self.latest_successful_for(ref)
116
      success.latest(ref).order(id: :desc).first
117 118
    end

119 120 121 122 123 124
    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 已提交
125 126 127 128
    def self.truncate_sha(sha)
      sha[0...8]
    end

129
    def self.total_duration
L
Lin Jen-Shin 已提交
130
      where.not(duration: nil).sum(:duration)
131 132
    end

K
Kamil Trzcinski 已提交
133 134 135 136 137
    def stage(name)
      stage = Ci::Stage.new(self, name: name)
      stage unless stage.statuses_count.zero?
    end

138 139
    def stages_count
      statuses.select(:stage).distinct.count
K
Kamil Trzcinski 已提交
140 141
    end

K
Kamil Trzcinski 已提交
142
    def stages_name
143 144
      statuses.order(:stage_idx).distinct.
        pluck(:stage, :stage_idx).map(&:first)
K
Kamil Trzcinski 已提交
145 146
    end

147
    def stages
148 149
      # TODO, this needs refactoring, see gitlab-ce#26481.

D
Douwe Maan 已提交
150 151
      stages_query = statuses
        .group('stage').select(:stage).order('max(stage_idx)')
152

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

155
      warnings_sql = statuses.latest.select('COUNT(*)')
D
Douwe Maan 已提交
156
        .where('stage=sg.stage').failed_but_allowed.to_sql
157

D
Douwe Maan 已提交
158 159
      stages_with_statuses = CommitStatus.from(stages_query, :sg)
        .pluck('sg.stage', status_sql, "(#{warnings_sql})")
K
Kamil Trzcinski 已提交
160 161

      stages_with_statuses.map do |stage|
162
        Ci::Stage.new(self, Hash[%i[name status warnings].zip(stage)])
K
Kamil Trzcinski 已提交
163 164 165 166
      end
    end

    def artifacts
167
      builds.latest.with_artifacts_not_expired.includes(project: [:namespace])
K
Kamil Trzcinski 已提交
168 169
    end

D
Douwe Maan 已提交
170
    def valid_commit_sha
171
      if self.sha == Gitlab::Git::BLANK_SHA
D
Douwe Maan 已提交
172 173 174 175 176
        self.errors.add(:sha, " cant be 00000000 (branch removal)")
      end
    end

    def git_author_name
177
      commit.try(:author_name)
D
Douwe Maan 已提交
178 179 180
    end

    def git_author_email
181
      commit.try(:author_email)
D
Douwe Maan 已提交
182 183 184
    end

    def git_commit_message
185
      commit.try(:message)
D
Douwe Maan 已提交
186 187
    end

188 189 190 191
    def git_commit_title
      commit.try(:title)
    end

D
Douwe Maan 已提交
192
    def short_sha
193
      Ci::Pipeline.truncate_sha(sha)
D
Douwe Maan 已提交
194 195
    end

196
    def commit
K
Kamil Trzcinski 已提交
197
      @commit ||= project.commit(sha)
D
Douwe Maan 已提交
198 199 200 201
    rescue
      nil
    end

K
Kamil Trzcinski 已提交
202 203 204 205
    def branch?
      !tag?
    end

206
    def manual_actions
207 208 209 210
      builds.latest.manual_actions.includes(project: [:namespace])
    end

    def stuck?
211
      builds.pending.includes(:project).any?(&:stuck?)
212 213
    end

K
Kamil Trzcinski 已提交
214
    def retryable?
215
      builds.latest.failed_or_canceled.any?(&:retryable?)
K
Kamil Trzcinski 已提交
216 217
    end

218
    def cancelable?
219
      statuses.cancelable.any?
220 221
    end

L
Lin Jen-Shin 已提交
222 223 224 225
    def auto_canceled?
      canceled? && auto_canceled_by_id?
    end

K
Kamil Trzcinski 已提交
226
    def cancel_running
227 228
      Gitlab::OptimisticLocking.retry_lock(
        statuses.cancelable) do |cancelable|
229
          cancelable.find_each(&:cancel)
230
        end
K
Kamil Trzcinski 已提交
231 232
    end

233
    def retry_failed(current_user)
D
Douwe Maan 已提交
234 235
      Ci::RetryPipelineService.new(project, current_user)
        .execute(self)
K
Kamil Trzcinski 已提交
236 237
    end

238
    def mark_as_processable_after_stage(stage_idx)
239
      builds.skipped.after_stage(stage_idx).find_each(&:process)
240 241
    end

K
Kamil Trzcinski 已提交
242 243 244 245 246 247 248
    def latest?
      return false unless ref
      commit = project.commit(ref)
      return false unless commit
      commit.sha == sha
    end

K
Kamil Trzcinski 已提交
249 250 251 252
    def triggered?
      trigger_requests.any?
    end

K
Kamil Trzcinski 已提交
253 254
    def retried
      @retried ||= (statuses.order(id: :desc) - statuses.latest)
D
Douwe Maan 已提交
255 256 257
    end

    def coverage
258
      coverage_array = statuses.latest.map(&:coverage).compact
K
Kamil Trzcinski 已提交
259 260
      if coverage_array.size >= 1
        '%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
D
Douwe Maan 已提交
261 262 263
      end
    end

264 265 266
    def config_builds_attributes
      return [] unless config_processor

267 268 269
      config_processor.
        builds_for_ref(ref, tag?, trigger_requests.first).
        sort_by { |build| build[:stage_idx] }
270 271
    end

C
Connor Shea 已提交
272
    def has_warnings?
273
      builds.latest.failed_but_allowed.any?
274 275
    end

D
Douwe Maan 已提交
276
    def config_processor
277
      return nil unless ci_yaml_file
278 279 280 281 282
      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
283
        self.yaml_errors = e.message
284 285
        nil
      rescue
286
        self.yaml_errors = 'Undefined error'
287 288
        nil
      end
D
Douwe Maan 已提交
289 290
    end

K
Kamil Trzcinski 已提交
291
    def ci_yaml_file
292 293
      return @ci_yaml_file if defined?(@ci_yaml_file)

D
Douwe Maan 已提交
294
      @ci_yaml_file = project.repository.gitlab_ci_yml_for(sha) rescue nil
K
Kamil Trzcinski 已提交
295 296
    end

297 298 299 300
    def has_yaml_errors?
      yaml_errors.present?
    end

K
Kamil Trzcinski 已提交
301 302 303 304
    def environments
      builds.where.not(environment: nil).success.pluck(:environment).uniq
    end

J
James Lopez 已提交
305 306 307 308 309 310 311 312 313 314 315 316 317
    # 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

318 319 320 321
    def notes
      Note.for_commit_id(sha)
    end

322 323 324
    def process!
      Ci::ProcessPipelineService.new(project, user).execute(self)
    end
325

326
    def update_status
K
Kamil Trzcinski 已提交
327
      Gitlab::OptimisticLocking.retry_lock(self) do
K
Kamil Trzcinski 已提交
328
        case latest_builds_status
329 330 331 332 333 334
        when 'pending' then enqueue
        when 'running' then run
        when 'success' then succeed
        when 'failed' then drop
        when 'canceled' then cancel
        when 'skipped' then skip
335
        when 'manual' then block
336
        end
337
      end
338
      refresh_build_status_cache
339 340
    end

341 342 343 344 345 346
    def predefined_variables
      [
        { key: 'CI_PIPELINE_ID', value: id.to_s, public: true }
      ]
    end

347 348 349 350 351 352 353
    def queued_duration
      return unless started_at

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

354
    def update_duration
355 356
      return unless started_at

357
      self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
358 359 360
    end

    def execute_hooks
K
Kamil Trzcinski 已提交
361 362 363
      data = pipeline_data
      project.execute_hooks(data, :pipeline_hooks)
      project.execute_services(data, :pipeline_hooks)
364 365
    end

366 367 368
    # Merge requests for which the current pipeline is running against
    # the merge request's latest commit.
    def merge_requests
D
Douwe Maan 已提交
369 370 371
      @merge_requests ||= project.merge_requests
        .where(source_branch: self.ref)
        .select { |merge_request| merge_request.head_pipeline.try(:id) == self.id }
372 373
    end

374
    def detailed_status(current_user)
D
Douwe Maan 已提交
375 376 377
      Gitlab::Ci::Status::Pipeline::Factory
        .new(self, current_user)
        .fabricate!
378 379
    end

380 381 382 383
    def refresh_build_status_cache
      Ci::PipelineStatus.new(project, sha: sha, status: status).store_in_cache_if_needed
    end

384 385
    private

386
    def pipeline_data
387
      Gitlab::DataBuilder::Pipeline.build(self)
K
Kamil Trzcinski 已提交
388
    end
389

390
    def latest_builds_status
391 392 393
      return 'failed' unless yaml_errors.blank?

      statuses.latest.status || 'skipped'
K
Kamil Trzcinski 已提交
394
    end
395 396

    def keep_around_commits
397
      return unless project
398

399 400 401
      project.repository.keep_around(self.sha)
      project.repository.keep_around(self.before_sha)
    end
D
Douwe Maan 已提交
402 403
  end
end