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

K
Kamil Trzcinski 已提交
222
    def cancel_running
223 224
      Gitlab::OptimisticLocking.retry_lock(
        statuses.cancelable) do |cancelable|
225
          cancelable.find_each(&:cancel)
226
        end
K
Kamil Trzcinski 已提交
227 228
    end

229
    def retry_failed(current_user)
D
Douwe Maan 已提交
230 231
      Ci::RetryPipelineService.new(project, current_user)
        .execute(self)
K
Kamil Trzcinski 已提交
232 233
    end

234
    def mark_as_processable_after_stage(stage_idx)
235
      builds.skipped.after_stage(stage_idx).find_each(&:process)
236 237
    end

K
Kamil Trzcinski 已提交
238 239 240 241 242 243 244
    def latest?
      return false unless ref
      commit = project.commit(ref)
      return false unless commit
      commit.sha == sha
    end

K
Kamil Trzcinski 已提交
245 246 247 248
    def triggered?
      trigger_requests.any?
    end

K
Kamil Trzcinski 已提交
249 250
    def retried
      @retried ||= (statuses.order(id: :desc) - statuses.latest)
D
Douwe Maan 已提交
251 252 253
    end

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

260 261 262
    def config_builds_attributes
      return [] unless config_processor

263 264 265
      config_processor.
        builds_for_ref(ref, tag?, trigger_requests.first).
        sort_by { |build| build[:stage_idx] }
266 267
    end

C
Connor Shea 已提交
268
    def has_warnings?
269
      builds.latest.failed_but_allowed.any?
270 271
    end

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

K
Kamil Trzcinski 已提交
287
    def ci_yaml_file
288 289
      return @ci_yaml_file if defined?(@ci_yaml_file)

D
Douwe Maan 已提交
290
      @ci_yaml_file = project.repository.gitlab_ci_yml_for(sha) rescue nil
K
Kamil Trzcinski 已提交
291 292
    end

293 294 295 296
    def has_yaml_errors?
      yaml_errors.present?
    end

K
Kamil Trzcinski 已提交
297 298 299 300
    def environments
      builds.where.not(environment: nil).success.pluck(:environment).uniq
    end

J
James Lopez 已提交
301 302 303 304 305 306 307 308 309 310 311 312 313
    # 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

314 315 316 317
    def notes
      Note.for_commit_id(sha)
    end

318 319 320
    def process!
      Ci::ProcessPipelineService.new(project, user).execute(self)
    end
321

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

337 338 339 340 341 342
    def predefined_variables
      [
        { key: 'CI_PIPELINE_ID', value: id.to_s, public: true }
      ]
    end

343 344 345 346 347 348 349
    def queued_duration
      return unless started_at

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

350
    def update_duration
351 352
      return unless started_at

353
      self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
354 355 356
    end

    def execute_hooks
K
Kamil Trzcinski 已提交
357 358 359
      data = pipeline_data
      project.execute_hooks(data, :pipeline_hooks)
      project.execute_services(data, :pipeline_hooks)
360 361
    end

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

370
    def detailed_status(current_user)
D
Douwe Maan 已提交
371 372 373
      Gitlab::Ci::Status::Pipeline::Factory
        .new(self, current_user)
        .fabricate!
374 375
    end

376 377 378 379
    def refresh_build_status_cache
      Ci::PipelineStatus.new(project, sha: sha, status: status).store_in_cache_if_needed
    end

380 381
    private

382
    def pipeline_data
383
      Gitlab::DataBuilder::Pipeline.build(self)
K
Kamil Trzcinski 已提交
384
    end
385

386
    def latest_builds_status
387 388 389
      return 'failed' unless yaml_errors.blank?

      statuses.latest.status || 'skipped'
K
Kamil Trzcinski 已提交
390
    end
391 392

    def keep_around_commits
393
      return unless project
394

395 396 397
      project.repository.keep_around(self.sha)
      project.repository.keep_around(self.before_sha)
    end
D
Douwe Maan 已提交
398 399
  end
end