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
F
Felipe Artur 已提交
20 21 22

    # Merge requests for which the current pipeline is running against
    # the merge request's latest commit.
23
    has_many :merge_requests, foreign_key: "head_pipeline_id"
D
Douwe Maan 已提交
24

25 26 27 28 29 30
    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 已提交
31 32
    delegate :id, to: :project, prefix: true

D
Douwe Maan 已提交
33 34 35
    validates :sha, presence: { unless: :importing? }
    validates :ref, presence: { unless: :importing? }
    validates :status, presence: { unless: :importing? }
36
    validate :valid_commit_sha, unless: :importing?
D
Douwe Maan 已提交
37

38
    after_create :keep_around_commits, unless: :importing?
K
Kamil Trzcinski 已提交
39

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

      event :run do
K
Kamil Trzcinski 已提交
47
        transition any - [:running] => :running
48 49
      end

50
      event :skip do
K
Kamil Trzcinski 已提交
51
        transition any - [:skipped] => :skipped
52 53 54
      end

      event :drop do
K
Kamil Trzcinski 已提交
55
        transition any - [:failed] => :failed
56 57
      end

58
      event :succeed do
K
Kamil Trzcinski 已提交
59
        transition any - [:success] => :success
60 61 62
      end

      event :cancel do
K
Kamil Trzcinski 已提交
63
        transition any - [:canceled] => :canceled
64 65
      end

66
      event :block do
67
        transition any - [:manual] => :manual
68 69
      end

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

74
      before_transition [:created, :pending] => :running do |pipeline|
75
        pipeline.started_at = Time.now
76 77
      end

78
      before_transition any => [:success, :failed, :canceled] do |pipeline|
79
        pipeline.finished_at = Time.now
80 81 82
        pipeline.update_duration
      end

83 84 85 86
      before_transition any => [:manual] do |pipeline|
        pipeline.update_duration
      end

L
Lin Jen-Shin 已提交
87
      before_transition canceled: any - [:canceled] do |pipeline|
L
Lin Jen-Shin 已提交
88 89 90
        pipeline.auto_canceled_by = nil
      end

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

      after_transition any => [:success] do |pipeline|
96
        pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
97 98
      end

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

      after_transition do |pipeline, transition|
104 105 106
        next if transition.loopback?

        pipeline.run_after_commit do
107
          PipelineHooksWorker.perform_async(pipeline.id)
108
          ExpirePipelineCacheWorker.perform_async(pipeline.id)
109
        end
110
      end
111

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

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

125 126 127 128 129
      if ref
        where(ref: ref, id: max_id.where(ref: ref))
      else
        where(id: max_id)
      end
130
    end
131

132 133 134 135
    def self.latest_status(ref = nil)
      latest(ref).status
    end

136
    def self.latest_successful_for(ref)
137
      success.latest(ref).order(id: :desc).first
138 139
    end

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

150
    def self.total_duration
L
Lin Jen-Shin 已提交
151
      where.not(duration: nil).sum(:duration)
152 153
    end

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

159 160
    def stages_count
      statuses.select(:stage).distinct.count
K
Kamil Trzcinski 已提交
161 162
    end

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

168
    def stages
169 170
      # TODO, this needs refactoring, see gitlab-ce#26481.

D
Douwe Maan 已提交
171 172
      stages_query = statuses
        .group('stage').select(:stage).order('max(stage_idx)')
173

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

176
      warnings_sql = statuses.latest.select('COUNT(*)')
D
Douwe Maan 已提交
177
        .where('stage=sg.stage').failed_but_allowed.to_sql
178

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

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

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

    def git_author_name
194
      commit.try(:author_name)
D
Douwe Maan 已提交
195 196 197
    end

    def git_author_email
198
      commit.try(:author_email)
D
Douwe Maan 已提交
199 200 201
    end

    def git_commit_message
202
      commit.try(:message)
D
Douwe Maan 已提交
203 204
    end

205 206 207 208
    def git_commit_title
      commit.try(:title)
    end

D
Douwe Maan 已提交
209
    def short_sha
210
      Ci::Pipeline.truncate_sha(sha)
D
Douwe Maan 已提交
211 212
    end

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

K
Kamil Trzcinski 已提交
219 220 221 222
    def branch?
      !tag?
    end

223
    def stuck?
224
      pending_builds.any?(&:stuck?)
225 226
    end

K
Kamil Trzcinski 已提交
227
    def retryable?
228
      retryable_builds.any?
K
Kamil Trzcinski 已提交
229 230
    end

231
    def cancelable?
232
      cancelable_statuses.any?
233 234
    end

L
Lin Jen-Shin 已提交
235 236 237 238
    def auto_canceled?
      canceled? && auto_canceled_by_id?
    end

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

248 249 250 251 252
    def auto_cancel_running(pipeline)
      update(auto_canceled_by: pipeline)

      cancel_running do |job|
        job.auto_canceled_by = pipeline
253
      end
K
Kamil Trzcinski 已提交
254 255
    end

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

261
    def mark_as_processable_after_stage(stage_idx)
262
      builds.skipped.after_stage(stage_idx).find_each(&:process)
263 264
    end

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

K
Kamil Trzcinski 已提交
272 273 274 275
    def triggered?
      trigger_requests.any?
    end

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

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

287 288 289
    ##
    # TODO, phase this method out
    #
290 291 292
    def config_builds_attributes
      return [] unless config_processor

293 294 295
      config_processor.
        builds_for_ref(ref, tag?, trigger_requests.first).
        sort_by { |build| build[:stage_idx] }
296 297
    end

298 299 300 301 302 303 304 305 306 307
    def config_stages_attributes
      return [] unless config_processor

      config_processor.stages_for_ref(ref, tag?, trigger_requests.first)
    end

    def has_stages?
      config_stages_attributes.any?
    end

C
Connor Shea 已提交
308
    def has_warnings?
309
      builds.latest.failed_but_allowed.any?
310 311
    end

D
Douwe Maan 已提交
312
    def config_processor
313
      return nil unless ci_yaml_file
314 315 316 317 318
      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
319
        self.yaml_errors = e.message
320 321
        nil
      rescue
322
        self.yaml_errors = 'Undefined error'
323 324
        nil
      end
D
Douwe Maan 已提交
325 326
    end

K
Kamil Trzcinski 已提交
327
    def ci_yaml_file
328 329
      return @ci_yaml_file if defined?(@ci_yaml_file)

D
Douwe Maan 已提交
330
      @ci_yaml_file = project.repository.gitlab_ci_yml_for(sha) rescue nil
K
Kamil Trzcinski 已提交
331 332
    end

333 334 335 336
    def has_yaml_errors?
      yaml_errors.present?
    end

K
Kamil Trzcinski 已提交
337 338 339 340
    def environments
      builds.where.not(environment: nil).success.pluck(:environment).uniq
    end

J
James Lopez 已提交
341 342 343 344 345 346 347 348 349 350 351 352 353
    # 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

354 355 356 357
    def notes
      Note.for_commit_id(sha)
    end

358 359 360
    def process!
      Ci::ProcessPipelineService.new(project, user).execute(self)
    end
361

362
    def update_status
K
Kamil Trzcinski 已提交
363
      Gitlab::OptimisticLocking.retry_lock(self) do
K
Kamil Trzcinski 已提交
364
        case latest_builds_status
365 366 367 368 369 370
        when 'pending' then enqueue
        when 'running' then run
        when 'success' then succeed
        when 'failed' then drop
        when 'canceled' then cancel
        when 'skipped' then skip
371
        when 'manual' then block
372
        end
373
      end
374 375
    end

376 377 378 379 380 381
    def predefined_variables
      [
        { key: 'CI_PIPELINE_ID', value: id.to_s, public: true }
      ]
    end

382 383 384 385 386 387 388
    def queued_duration
      return unless started_at

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

389
    def update_duration
390 391
      return unless started_at

392
      self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
393 394 395
    end

    def execute_hooks
K
Kamil Trzcinski 已提交
396 397 398
      data = pipeline_data
      project.execute_hooks(data, :pipeline_hooks)
      project.execute_services(data, :pipeline_hooks)
399 400
    end

401 402
    # All the merge requests for which the current pipeline runs/ran against
    def all_merge_requests
403
      @all_merge_requests ||= project.merge_requests.where(source_branch: ref)
404 405
    end

406
    def detailed_status(current_user)
D
Douwe Maan 已提交
407 408 409
      Gitlab::Ci::Status::Pipeline::Factory
        .new(self, current_user)
        .fabricate!
410 411
    end

412 413
    private

414
    def pipeline_data
415
      Gitlab::DataBuilder::Pipeline.build(self)
K
Kamil Trzcinski 已提交
416
    end
417

418
    def latest_builds_status
419 420 421
      return 'failed' unless yaml_errors.blank?

      statuses.latest.status || 'skipped'
K
Kamil Trzcinski 已提交
422
    end
423 424

    def keep_around_commits
425
      return unless project
426

427 428 429
      project.repository.keep_around(self.sha)
      project.repository.keep_around(self.before_sha)
    end
D
Douwe Maan 已提交
430 431
  end
end