pipeline.rb 11.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
    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
D
Douwe Maan 已提交
20

21 22 23 24 25 26
    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 已提交
27 28
    delegate :id, to: :project, prefix: true

D
Douwe Maan 已提交
29 30 31
    validates :sha, presence: { unless: :importing? }
    validates :ref, presence: { unless: :importing? }
    validates :status, presence: { unless: :importing? }
32
    validate :valid_commit_sha, unless: :importing?
D
Douwe Maan 已提交
33

34
    after_create :keep_around_commits, unless: :importing?
K
Kamil Trzcinski 已提交
35

36
    state_machine :status, initial: :created do
37
      event :enqueue do
K
Kamil Trzcinski 已提交
38
        transition created: :pending
39
        transition [:success, :failed, :canceled, :skipped] => :running
40 41 42
      end

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

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

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

54
      event :succeed do
K
Kamil Trzcinski 已提交
55
        transition any - [:success] => :success
56 57 58
      end

      event :cancel do
K
Kamil Trzcinski 已提交
59
        transition any - [:canceled] => :canceled
60 61
      end

62
      event :block do
63
        transition any - [:manual] => :manual
64 65
      end

K
Kamil Trzcinski 已提交
66 67 68 69
      # IMPORTANT
      # Do not add any operations to this state_machine
      # Create a separate worker for each new operation

70
      before_transition [:created, :pending] => :running do |pipeline|
71
        pipeline.started_at = Time.now
72 73
      end

74
      before_transition any => [:success, :failed, :canceled] do |pipeline|
75
        pipeline.finished_at = Time.now
76 77 78
        pipeline.update_duration
      end

79 80 81 82
      before_transition any => [:manual] do |pipeline|
        pipeline.update_duration
      end

L
Lin Jen-Shin 已提交
83
      before_transition canceled: any - [:canceled] do |pipeline|
L
Lin Jen-Shin 已提交
84 85 86
        pipeline.auto_canceled_by = nil
      end

87
      after_transition [:created, :pending] => :running do |pipeline|
88
        pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
89 90 91
      end

      after_transition any => [:success] do |pipeline|
92
        pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
93 94
      end

95
      after_transition [:created, :pending, :running] => :success do |pipeline|
96
        pipeline.run_after_commit { PipelineSuccessWorker.perform_async(pipeline.id) }
97
      end
98 99

      after_transition do |pipeline, transition|
100 101 102
        next if transition.loopback?

        pipeline.run_after_commit do
103
          PipelineHooksWorker.perform_async(pipeline.id)
104
          ExpirePipelineCacheWorker.perform_async(pipeline.id)
105
        end
106
      end
107

108
      after_transition any => [:success, :failed] do |pipeline|
109
        pipeline.run_after_commit do
110
          PipelineNotificationWorker.perform_async(pipeline.id)
111
        end
112
      end
113 114
    end

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

121 122 123 124 125
      if ref
        where(ref: ref, id: max_id.where(ref: ref))
      else
        where(id: max_id)
      end
126
    end
127

128 129 130 131
    def self.latest_status(ref = nil)
      latest(ref).status
    end

132
    def self.latest_successful_for(ref)
133
      success.latest(ref).order(id: :desc).first
134 135
    end

136 137 138 139 140 141
    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 已提交
142 143 144 145
    def self.truncate_sha(sha)
      sha[0...8]
    end

146
    def self.total_duration
L
Lin Jen-Shin 已提交
147
      where.not(duration: nil).sum(:duration)
148 149
    end

K
Kamil Trzcinski 已提交
150 151 152 153 154
    def stage(name)
      stage = Ci::Stage.new(self, name: name)
      stage unless stage.statuses_count.zero?
    end

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

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

164
    def stages
165 166
      # TODO, this needs refactoring, see gitlab-ce#26481.

D
Douwe Maan 已提交
167 168
      stages_query = statuses
        .group('stage').select(:stage).order('max(stage_idx)')
169

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

172
      warnings_sql = statuses.latest.select('COUNT(*)')
D
Douwe Maan 已提交
173
        .where('stage=sg.stage').failed_but_allowed.to_sql
174

D
Douwe Maan 已提交
175 176
      stages_with_statuses = CommitStatus.from(stages_query, :sg)
        .pluck('sg.stage', status_sql, "(#{warnings_sql})")
K
Kamil Trzcinski 已提交
177 178

      stages_with_statuses.map do |stage|
179
        Ci::Stage.new(self, Hash[%i[name status warnings].zip(stage)])
K
Kamil Trzcinski 已提交
180 181 182
      end
    end

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

    def git_author_name
190
      commit.try(:author_name)
D
Douwe Maan 已提交
191 192 193
    end

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

    def git_commit_message
198
      commit.try(:message)
D
Douwe Maan 已提交
199 200
    end

201 202 203 204
    def git_commit_title
      commit.try(:title)
    end

D
Douwe Maan 已提交
205
    def short_sha
206
      Ci::Pipeline.truncate_sha(sha)
D
Douwe Maan 已提交
207 208
    end

209
    def commit
K
Kamil Trzcinski 已提交
210
      @commit ||= project.commit(sha)
D
Douwe Maan 已提交
211 212 213 214
    rescue
      nil
    end

K
Kamil Trzcinski 已提交
215 216 217 218
    def branch?
      !tag?
    end

219
    def stuck?
220
      pending_builds.any?(&:stuck?)
221 222
    end

K
Kamil Trzcinski 已提交
223
    def retryable?
224
      retryable_builds.any?
K
Kamil Trzcinski 已提交
225 226
    end

227
    def cancelable?
228
      cancelable_statuses.any?
229 230
    end

L
Lin Jen-Shin 已提交
231 232 233 234
    def auto_canceled?
      canceled? && auto_canceled_by_id?
    end

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

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

      cancel_running do |job|
        job.auto_canceled_by = pipeline
249
      end
K
Kamil Trzcinski 已提交
250 251
    end

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

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

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

K
Kamil Trzcinski 已提交
268 269 270 271
    def triggered?
      trigger_requests.any?
    end

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

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

283 284 285
    def config_builds_attributes
      return [] unless config_processor

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

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

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

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

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

316 317 318 319
    def has_yaml_errors?
      yaml_errors.present?
    end

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

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

337 338 339 340
    def notes
      Note.for_commit_id(sha)
    end

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

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

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

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

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

372
    def update_duration
373 374
      return unless started_at

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

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

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

392 393
    # All the merge requests for which the current pipeline runs/ran against
    def all_merge_requests
394
      @all_merge_requests ||= project.merge_requests.where(source_branch: ref)
395 396
    end

397
    def detailed_status(current_user)
D
Douwe Maan 已提交
398 399 400
      Gitlab::Ci::Status::Pipeline::Factory
        .new(self, current_user)
        .fabricate!
401 402
    end

403 404
    private

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

409
    def latest_builds_status
410 411 412
      return 'failed' unless yaml_errors.blank?

      statuses.latest.status || 'skipped'
K
Kamil Trzcinski 已提交
413
    end
414 415

    def keep_around_commits
416
      return unless project
417

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