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 :stages
15
    has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
16 17
    has_many :builds, foreign_key: :commit_id
    has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id
F
Felipe Artur 已提交
18 19 20

    # Merge requests for which the current pipeline is running against
    # the merge request's latest commit.
21
    has_many :merge_requests, foreign_key: "head_pipeline_id"
D
Douwe Maan 已提交
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'
26 27
    has_many :manual_actions, -> { latest.manual_actions.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build'
    has_many :artifacts, -> { latest.with_artifacts_not_expired.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build'
28

29 30 31
    has_many :auto_canceled_pipelines, class_name: 'Ci::Pipeline', foreign_key: 'auto_canceled_by_id'
    has_many :auto_canceled_jobs, class_name: 'CommitStatus', foreign_key: 'auto_canceled_by_id'

D
Douwe Maan 已提交
32 33
    delegate :id, to: :project, prefix: true

34
    validates :source, exclusion: { in: %w(unknown), unless: :importing? }, on: :create
D
Douwe Maan 已提交
35 36 37
    validates :sha, presence: { unless: :importing? }
    validates :ref, presence: { unless: :importing? }
    validates :status, presence: { unless: :importing? }
38
    validate :valid_commit_sha, unless: :importing?
D
Douwe Maan 已提交
39

40
    after_create :keep_around_commits, unless: :importing?
K
Kamil Trzcinski 已提交
41

42 43 44 45 46 47 48 49 50 51
    enum source: {
      unknown: nil,
      push: 1,
      web: 2,
      trigger: 3,
      schedule: 4,
      api: 5,
      external: 6
    }

52
    state_machine :status, initial: :created do
53
      event :enqueue do
K
Kamil Trzcinski 已提交
54
        transition created: :pending
55
        transition [:success, :failed, :canceled, :skipped] => :running
56 57 58
      end

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

62
      event :skip do
K
Kamil Trzcinski 已提交
63
        transition any - [:skipped] => :skipped
64 65 66
      end

      event :drop do
K
Kamil Trzcinski 已提交
67
        transition any - [:failed] => :failed
68 69
      end

70
      event :succeed do
K
Kamil Trzcinski 已提交
71
        transition any - [:success] => :success
72 73 74
      end

      event :cancel do
K
Kamil Trzcinski 已提交
75
        transition any - [:canceled] => :canceled
76 77
      end

78
      event :block do
79
        transition any - [:manual] => :manual
80 81
      end

K
Kamil Trzcinski 已提交
82 83 84 85
      # IMPORTANT
      # Do not add any operations to this state_machine
      # Create a separate worker for each new operation

86
      before_transition [:created, :pending] => :running do |pipeline|
87
        pipeline.started_at = Time.now
88 89
      end

90
      before_transition any => [:success, :failed, :canceled] do |pipeline|
91
        pipeline.finished_at = Time.now
92 93 94
        pipeline.update_duration
      end

95 96 97 98
      before_transition any => [:manual] do |pipeline|
        pipeline.update_duration
      end

L
Lin Jen-Shin 已提交
99
      before_transition canceled: any - [:canceled] do |pipeline|
L
Lin Jen-Shin 已提交
100 101 102
        pipeline.auto_canceled_by = nil
      end

103
      after_transition [:created, :pending] => :running do |pipeline|
104
        pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
105 106 107
      end

      after_transition any => [:success] do |pipeline|
108
        pipeline.run_after_commit { PipelineMetricsWorker.perform_async(pipeline.id) }
109 110
      end

111
      after_transition [:created, :pending, :running] => :success do |pipeline|
112
        pipeline.run_after_commit { PipelineSuccessWorker.perform_async(pipeline.id) }
113
      end
114 115

      after_transition do |pipeline, transition|
116 117 118
        next if transition.loopback?

        pipeline.run_after_commit do
119
          PipelineHooksWorker.perform_async(pipeline.id)
120
          ExpirePipelineCacheWorker.perform_async(pipeline.id)
121
        end
122
      end
123

124
      after_transition any => [:success, :failed] do |pipeline|
125
        pipeline.run_after_commit do
126
          PipelineNotificationWorker.perform_async(pipeline.id)
127
        end
128
      end
129 130
    end

131
    # ref can't be HEAD or SHA, can only be branch/tag name
132
    scope :latest, ->(ref = nil) do
D
Douwe Maan 已提交
133 134 135
      max_id = unscope(:select)
        .select("max(#{quoted_table_name}.id)")
        .group(:ref, :sha)
136

137 138 139 140 141
      if ref
        where(ref: ref, id: max_id.where(ref: ref))
      else
        where(id: max_id)
      end
142
    end
143

144 145 146 147
    def self.latest_status(ref = nil)
      latest(ref).status
    end

148
    def self.latest_successful_for(ref)
149
      success.latest(ref).order(id: :desc).first
150 151
    end

152 153 154 155 156 157
    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 已提交
158 159 160 161
    def self.truncate_sha(sha)
      sha[0...8]
    end

162
    def self.total_duration
L
Lin Jen-Shin 已提交
163
      where.not(duration: nil).sum(:duration)
164 165
    end

166 167
    def stages_count
      statuses.select(:stage).distinct.count
K
Kamil Trzcinski 已提交
168 169
    end

170
    def stages_names
171 172
      statuses.order(:stage_idx).distinct.
        pluck(:stage, :stage_idx).map(&:first)
K
Kamil Trzcinski 已提交
173 174
    end

175
    def legacy_stage(name)
176
      stage = Ci::LegacyStage.new(self, name: name)
177 178 179 180
      stage unless stage.statuses_count.zero?
    end

    def legacy_stages
181 182
      # TODO, this needs refactoring, see gitlab-ce#26481.

D
Douwe Maan 已提交
183 184
      stages_query = statuses
        .group('stage').select(:stage).order('max(stage_idx)')
185

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

188
      warnings_sql = statuses.latest.select('COUNT(*)')
D
Douwe Maan 已提交
189
        .where('stage=sg.stage').failed_but_allowed.to_sql
190

D
Douwe Maan 已提交
191 192
      stages_with_statuses = CommitStatus.from(stages_query, :sg)
        .pluck('sg.stage', status_sql, "(#{warnings_sql})")
K
Kamil Trzcinski 已提交
193 194

      stages_with_statuses.map do |stage|
195
        Ci::LegacyStage.new(self, Hash[%i[name status warnings].zip(stage)])
K
Kamil Trzcinski 已提交
196 197 198
      end
    end

D
Douwe Maan 已提交
199
    def valid_commit_sha
200
      if self.sha == Gitlab::Git::BLANK_SHA
D
Douwe Maan 已提交
201 202 203 204 205
        self.errors.add(:sha, " cant be 00000000 (branch removal)")
      end
    end

    def git_author_name
206
      commit.try(:author_name)
D
Douwe Maan 已提交
207 208 209
    end

    def git_author_email
210
      commit.try(:author_email)
D
Douwe Maan 已提交
211 212 213
    end

    def git_commit_message
214
      commit.try(:message)
D
Douwe Maan 已提交
215 216
    end

217 218 219 220
    def git_commit_title
      commit.try(:title)
    end

D
Douwe Maan 已提交
221
    def short_sha
222
      Ci::Pipeline.truncate_sha(sha)
D
Douwe Maan 已提交
223 224
    end

225
    def commit
K
Kamil Trzcinski 已提交
226
      @commit ||= project.commit(sha)
D
Douwe Maan 已提交
227 228 229 230
    rescue
      nil
    end

K
Kamil Trzcinski 已提交
231 232 233 234
    def branch?
      !tag?
    end

235
    def stuck?
236
      pending_builds.any?(&:stuck?)
237 238
    end

K
Kamil Trzcinski 已提交
239
    def retryable?
240
      retryable_builds.any?
K
Kamil Trzcinski 已提交
241 242
    end

243
    def cancelable?
244
      cancelable_statuses.any?
245 246
    end

L
Lin Jen-Shin 已提交
247 248 249 250
    def auto_canceled?
      canceled? && auto_canceled_by_id?
    end

K
Kamil Trzcinski 已提交
251
    def cancel_running
252
      Gitlab::OptimisticLocking.retry_lock(cancelable_statuses) do |cancelable|
L
Lin Jen-Shin 已提交
253 254 255
        cancelable.find_each do |job|
          yield(job) if block_given?
          job.cancel
256
        end
L
Lin Jen-Shin 已提交
257
      end
K
Kamil Trzcinski 已提交
258 259
    end

260 261 262 263 264
    def auto_cancel_running(pipeline)
      update(auto_canceled_by: pipeline)

      cancel_running do |job|
        job.auto_canceled_by = pipeline
265
      end
K
Kamil Trzcinski 已提交
266 267
    end

268
    def retry_failed(current_user)
D
Douwe Maan 已提交
269 270
      Ci::RetryPipelineService.new(project, current_user)
        .execute(self)
K
Kamil Trzcinski 已提交
271 272
    end

273
    def mark_as_processable_after_stage(stage_idx)
274
      builds.skipped.after_stage(stage_idx).find_each(&:process)
275 276
    end

K
Kamil Trzcinski 已提交
277 278 279 280 281 282 283
    def latest?
      return false unless ref
      commit = project.commit(ref)
      return false unless commit
      commit.sha == sha
    end

K
Kamil Trzcinski 已提交
284 285
    def retried
      @retried ||= (statuses.order(id: :desc) - statuses.latest)
D
Douwe Maan 已提交
286 287 288
    end

    def coverage
289
      coverage_array = statuses.latest.map(&:coverage).compact
K
Kamil Trzcinski 已提交
290 291
      if coverage_array.size >= 1
        '%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
D
Douwe Maan 已提交
292 293 294
      end
    end

295
    def stage_seeds
296
      return [] unless config_processor
297

298
      @stage_seeds ||= config_processor.stage_seeds(self)
299 300
    end

301 302
    def has_stage_seeds?
      stage_seeds.any?
303 304
    end

C
Connor Shea 已提交
305
    def has_warnings?
306
      builds.latest.failed_but_allowed.any?
307 308
    end

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

K
Kamil Trzcinski 已提交
324
    def ci_yaml_file
325 326
      return @ci_yaml_file if defined?(@ci_yaml_file)

D
Douwe Maan 已提交
327
      @ci_yaml_file = project.repository.gitlab_ci_yml_for(sha) rescue nil
K
Kamil Trzcinski 已提交
328 329
    end

330 331 332 333
    def has_yaml_errors?
      yaml_errors.present?
    end

K
Kamil Trzcinski 已提交
334 335 336 337
    def environments
      builds.where.not(environment: nil).success.pluck(:environment).uniq
    end

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

351 352 353 354
    def notes
      Note.for_commit_id(sha)
    end

355 356 357
    def process!
      Ci::ProcessPipelineService.new(project, user).execute(self)
    end
358

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

373 374 375 376 377 378
    def predefined_variables
      [
        { key: 'CI_PIPELINE_ID', value: id.to_s, public: true }
      ]
    end

379 380 381 382 383 384 385
    def queued_duration
      return unless started_at

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

386
    def update_duration
387 388
      return unless started_at

389
      self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
390 391 392
    end

    def execute_hooks
K
Kamil Trzcinski 已提交
393 394 395
      data = pipeline_data
      project.execute_hooks(data, :pipeline_hooks)
      project.execute_services(data, :pipeline_hooks)
396 397
    end

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

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

409 410
    private

411
    def pipeline_data
412
      Gitlab::DataBuilder::Pipeline.build(self)
K
Kamil Trzcinski 已提交
413
    end
414

415
    def latest_builds_status
416 417 418
      return 'failed' unless yaml_errors.blank?

      statuses.latest.status || 'skipped'
K
Kamil Trzcinski 已提交
419
    end
420 421

    def keep_around_commits
422
      return unless project
423

424 425 426
      project.repository.keep_around(self.sha)
      project.repository.keep_around(self.before_sha)
    end
D
Douwe Maan 已提交
427 428
  end
end