pipeline.rb 10.8 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 15
    has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id'

16
    has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
17 18
    has_many :builds, foreign_key: :commit_id
    has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id
D
Douwe Maan 已提交
19

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

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

33
    after_create :keep_around_commits, unless: :importing?
34
    after_create :refresh_build_status_cache
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

L
Lin Jen-Shin 已提交
79
      before_transition canceled: any - [:canceled] do |pipeline|
L
Lin Jen-Shin 已提交
80 81 82
        pipeline.auto_canceled_by = nil
      end

83
      after_transition [:created, :pending] => :running do |pipeline|
K
Kamil Trzcinski 已提交
84
        pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) }
85 86 87
      end

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

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

      after_transition do |pipeline, transition|
96 97 98 99 100
        next if transition.loopback?

        pipeline.run_after_commit do
          PipelineHooksWorker.perform_async(id)
        end
101
      end
102

103
      after_transition any => [:success, :failed] do |pipeline|
104
        pipeline.run_after_commit do
105
          PipelineNotificationWorker.perform_async(pipeline.id)
106
        end
107
      end
108 109
    end

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

116 117 118 119 120
      if ref
        where(ref: ref, id: max_id.where(ref: ref))
      else
        where(id: max_id)
      end
121
    end
122

123 124 125 126
    def self.latest_status(ref = nil)
      latest(ref).status
    end

127
    def self.latest_successful_for(ref)
128
      success.latest(ref).order(id: :desc).first
129 130
    end

131 132 133 134 135 136
    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 已提交
137 138 139 140
    def self.truncate_sha(sha)
      sha[0...8]
    end

141
    def self.total_duration
L
Lin Jen-Shin 已提交
142
      where.not(duration: nil).sum(:duration)
143 144
    end

K
Kamil Trzcinski 已提交
145 146 147 148 149
    def stage(name)
      stage = Ci::Stage.new(self, name: name)
      stage unless stage.statuses_count.zero?
    end

150 151
    def stages_count
      statuses.select(:stage).distinct.count
K
Kamil Trzcinski 已提交
152 153
    end

K
Kamil Trzcinski 已提交
154
    def stages_name
155 156
      statuses.order(:stage_idx).distinct.
        pluck(:stage, :stage_idx).map(&:first)
K
Kamil Trzcinski 已提交
157 158
    end

159
    def stages
160 161
      # TODO, this needs refactoring, see gitlab-ce#26481.

D
Douwe Maan 已提交
162 163
      stages_query = statuses
        .group('stage').select(:stage).order('max(stage_idx)')
164

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

167
      warnings_sql = statuses.latest.select('COUNT(*)')
D
Douwe Maan 已提交
168
        .where('stage=sg.stage').failed_but_allowed.to_sql
169

D
Douwe Maan 已提交
170 171
      stages_with_statuses = CommitStatus.from(stages_query, :sg)
        .pluck('sg.stage', status_sql, "(#{warnings_sql})")
K
Kamil Trzcinski 已提交
172 173

      stages_with_statuses.map do |stage|
174
        Ci::Stage.new(self, Hash[%i[name status warnings].zip(stage)])
K
Kamil Trzcinski 已提交
175 176 177
      end
    end

D
Douwe Maan 已提交
178
    def valid_commit_sha
179
      if self.sha == Gitlab::Git::BLANK_SHA
D
Douwe Maan 已提交
180 181 182 183 184
        self.errors.add(:sha, " cant be 00000000 (branch removal)")
      end
    end

    def git_author_name
185
      commit.try(:author_name)
D
Douwe Maan 已提交
186 187 188
    end

    def git_author_email
189
      commit.try(:author_email)
D
Douwe Maan 已提交
190 191 192
    end

    def git_commit_message
193
      commit.try(:message)
D
Douwe Maan 已提交
194 195
    end

196 197 198 199
    def git_commit_title
      commit.try(:title)
    end

D
Douwe Maan 已提交
200
    def short_sha
201
      Ci::Pipeline.truncate_sha(sha)
D
Douwe Maan 已提交
202 203
    end

204
    def commit
K
Kamil Trzcinski 已提交
205
      @commit ||= project.commit(sha)
D
Douwe Maan 已提交
206 207 208 209
    rescue
      nil
    end

K
Kamil Trzcinski 已提交
210 211 212 213
    def branch?
      !tag?
    end

214
    def stuck?
215
      pending_builds.any?(&:stuck?)
216 217
    end

K
Kamil Trzcinski 已提交
218
    def retryable?
219
      retryable_builds.any?
K
Kamil Trzcinski 已提交
220 221
    end

222
    def cancelable?
223
      cancelable_statuses.any?
224 225
    end

L
Lin Jen-Shin 已提交
226 227 228 229
    def auto_canceled?
      canceled? && auto_canceled_by_id?
    end

K
Kamil Trzcinski 已提交
230
    def cancel_running
231 232
      Gitlab::OptimisticLocking.retry_lock(
        statuses.cancelable) do |cancelable|
233 234 235 236
          cancelable.find_each do |job|
            yield(job) if block_given?
            job.cancel
          end
237
        end
K
Kamil Trzcinski 已提交
238 239
    end

240 241 242 243 244 245 246 247
    def auto_cancel_running(pipeline)
      update(auto_canceled_by: pipeline)

      cancel_running do |job|
        job.auto_canceled_by = pipeline
      end
    end

248
    def retry_failed(current_user)
D
Douwe Maan 已提交
249 250
      Ci::RetryPipelineService.new(project, current_user)
        .execute(self)
K
Kamil Trzcinski 已提交
251 252
    end

253
    def mark_as_processable_after_stage(stage_idx)
254
      builds.skipped.after_stage(stage_idx).find_each(&:process)
255 256
    end

K
Kamil Trzcinski 已提交
257 258 259 260 261 262 263
    def latest?
      return false unless ref
      commit = project.commit(ref)
      return false unless commit
      commit.sha == sha
    end

K
Kamil Trzcinski 已提交
264 265 266 267
    def triggered?
      trigger_requests.any?
    end

K
Kamil Trzcinski 已提交
268 269
    def retried
      @retried ||= (statuses.order(id: :desc) - statuses.latest)
D
Douwe Maan 已提交
270 271 272
    end

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

279 280 281
    def config_builds_attributes
      return [] unless config_processor

282 283 284
      config_processor.
        builds_for_ref(ref, tag?, trigger_requests.first).
        sort_by { |build| build[:stage_idx] }
285 286
    end

C
Connor Shea 已提交
287
    def has_warnings?
288
      builds.latest.failed_but_allowed.any?
289 290
    end

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

K
Kamil Trzcinski 已提交
306
    def ci_yaml_file
307 308
      return @ci_yaml_file if defined?(@ci_yaml_file)

D
Douwe Maan 已提交
309
      @ci_yaml_file = project.repository.gitlab_ci_yml_for(sha) rescue nil
K
Kamil Trzcinski 已提交
310 311
    end

312 313 314 315
    def has_yaml_errors?
      yaml_errors.present?
    end

K
Kamil Trzcinski 已提交
316 317 318 319
    def environments
      builds.where.not(environment: nil).success.pluck(:environment).uniq
    end

J
James Lopez 已提交
320 321 322 323 324 325 326 327 328 329 330 331 332
    # 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

333 334 335 336
    def notes
      Note.for_commit_id(sha)
    end

337 338 339
    def process!
      Ci::ProcessPipelineService.new(project, user).execute(self)
    end
340

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

356 357 358 359 360 361
    def predefined_variables
      [
        { key: 'CI_PIPELINE_ID', value: id.to_s, public: true }
      ]
    end

362 363 364 365 366 367 368
    def queued_duration
      return unless started_at

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

369
    def update_duration
370 371
      return unless started_at

372
      self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
373 374 375
    end

    def execute_hooks
K
Kamil Trzcinski 已提交
376 377 378
      data = pipeline_data
      project.execute_hooks(data, :pipeline_hooks)
      project.execute_services(data, :pipeline_hooks)
379 380
    end

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

389
    def detailed_status(current_user)
D
Douwe Maan 已提交
390 391 392
      Gitlab::Ci::Status::Pipeline::Factory
        .new(self, current_user)
        .fabricate!
393 394
    end

395 396 397 398
    def refresh_build_status_cache
      Ci::PipelineStatus.new(project, sha: sha, status: status).store_in_cache_if_needed
    end

399 400
    private

401
    def pipeline_data
402
      Gitlab::DataBuilder::Pipeline.build(self)
K
Kamil Trzcinski 已提交
403
    end
404

405
    def latest_builds_status
406 407 408
      return 'failed' unless yaml_errors.blank?

      statuses.latest.status || 'skipped'
K
Kamil Trzcinski 已提交
409
    end
410 411

    def keep_around_commits
412
      return unless project
413

414 415 416
      project.repository.keep_around(self.sha)
      project.repository.keep_around(self.before_sha)
    end
D
Douwe Maan 已提交
417 418
  end
end