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

154 155
    def stages_count
      statuses.select(:stage).distinct.count
K
Kamil Trzcinski 已提交
156 157
    end

158
    def stages_names
159 160
      statuses.order(:stage_idx).distinct.
        pluck(:stage, :stage_idx).map(&:first)
K
Kamil Trzcinski 已提交
161 162
    end

163
    def legacy_stage(name)
164
      stage = Ci::LegacyStage.new(self, name: name)
165 166 167 168
      stage unless stage.statuses_count.zero?
    end

    def legacy_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::LegacyStage.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
    def stage_seeds
299 300 301
      return unless config_processor

      seeds_scope = { ref: ref, tag: tag?, trigger: trigger_requests.first }
302

303
      @seeds ||= config_processor.stage_seeds(seeds_scope).tap do |seeds|
304 305
        seeds.pipeline = self
      end
306 307 308
    end

    def has_stages?
309
      stage_seeds&.has_stages?
310 311
    end

C
Connor Shea 已提交
312
    def has_warnings?
313
      builds.latest.failed_but_allowed.any?
314 315
    end

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

K
Kamil Trzcinski 已提交
331
    def ci_yaml_file
332 333
      return @ci_yaml_file if defined?(@ci_yaml_file)

D
Douwe Maan 已提交
334
      @ci_yaml_file = project.repository.gitlab_ci_yml_for(sha) rescue nil
K
Kamil Trzcinski 已提交
335 336
    end

337 338 339 340
    def has_yaml_errors?
      yaml_errors.present?
    end

K
Kamil Trzcinski 已提交
341 342 343 344
    def environments
      builds.where.not(environment: nil).success.pluck(:environment).uniq
    end

J
James Lopez 已提交
345 346 347 348 349 350 351 352 353 354 355 356 357
    # 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

358 359 360 361
    def notes
      Note.for_commit_id(sha)
    end

362 363 364
    def process!
      Ci::ProcessPipelineService.new(project, user).execute(self)
    end
365

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

380 381 382 383 384 385
    def predefined_variables
      [
        { key: 'CI_PIPELINE_ID', value: id.to_s, public: true }
      ]
    end

386 387 388 389 390 391 392
    def queued_duration
      return unless started_at

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

393
    def update_duration
394 395
      return unless started_at

396
      self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
397 398 399
    end

    def execute_hooks
K
Kamil Trzcinski 已提交
400 401 402
      data = pipeline_data
      project.execute_hooks(data, :pipeline_hooks)
      project.execute_services(data, :pipeline_hooks)
403 404
    end

405 406
    # All the merge requests for which the current pipeline runs/ran against
    def all_merge_requests
407
      @all_merge_requests ||= project.merge_requests.where(source_branch: ref)
408 409
    end

410
    def detailed_status(current_user)
D
Douwe Maan 已提交
411 412 413
      Gitlab::Ci::Status::Pipeline::Factory
        .new(self, current_user)
        .fabricate!
414 415
    end

416 417
    private

418
    def pipeline_data
419
      Gitlab::DataBuilder::Pipeline.build(self)
K
Kamil Trzcinski 已提交
420
    end
421

422
    def latest_builds_status
423 424 425
      return 'failed' unless yaml_errors.blank?

      statuses.latest.status || 'skipped'
K
Kamil Trzcinski 已提交
426
    end
427 428

    def keep_around_commits
429
      return unless project
430

431 432 433
      project.repository.keep_around(self.sha)
      project.repository.keep_around(self.before_sha)
    end
D
Douwe Maan 已提交
434 435
  end
end