pipeline.rb 11.4 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
    enum source: {
      unknown: nil,
      push: 1,
      web: 2,
      trigger: 3,
      schedule: 4,
      api: 5,
      external: 6
    }
51

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
        pipeline.auto_canceled_by = nil
101 102
      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
    def self.latest_successful_for_refs(refs)
      success.latest(refs).order(id: :desc).each_with_object({}) do |pipeline, hash|
        hash[pipeline.ref] ||= pipeline
      end
D
Douwe Maan 已提交
156 157 158 159
    end

    def self.truncate_sha(sha)
      sha[0...8]
K
Kamil Trzcinski 已提交
160 161
    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)
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
      end
K
WIP  
Kamil Trzcinski 已提交
197 198
    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
    def auto_canceled?
      canceled? && auto_canceled_by_id?
249 250
    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 297
      return [] unless config_processor

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)

327 328
      @ci_yaml_file = begin
        project.repository.gitlab_ci_yml_for(sha, ci_yaml_file_path)
329 330
      rescue
        nil
331
      end
K
Kamil Trzcinski 已提交
332 333
    end

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

338 339 340 341 342 343 344
    def ci_yaml_file_path
      return '.gitlab-ci.yml' if project.ci_config_file.blank?
      return project.ci_config_file if File.extname(project.ci_config_file.to_s) == '.yml'

      File.join(project.ci_config_file || '', '.gitlab-ci.yml')
    end

K
Kamil Trzcinski 已提交
345 346 347 348
    def environments
      builds.where.not(environment: nil).success.pluck(:environment).uniq
    end

J
James Lopez 已提交
349 350 351 352 353 354 355 356 357 358 359 360 361
    # 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

362 363 364 365
    def notes
      Note.for_commit_id(sha)
    end

366 367 368
    def process!
      Ci::ProcessPipelineService.new(project, user).execute(self)
    end
369

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

384 385 386 387 388 389
    def predefined_variables
      [
        { key: 'CI_PIPELINE_ID', value: id.to_s, public: true }
      ]
    end

390 391 392 393 394 395 396
    def queued_duration
      return unless started_at

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

397
    def update_duration
398 399
      return unless started_at

400
      self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
401 402 403
    end

    def execute_hooks
K
Kamil Trzcinski 已提交
404 405 406
      data = pipeline_data
      project.execute_hooks(data, :pipeline_hooks)
      project.execute_services(data, :pipeline_hooks)
407 408
    end

409 410
    # All the merge requests for which the current pipeline runs/ran against
    def all_merge_requests
411
      @all_merge_requests ||= project.merge_requests.where(source_branch: ref)
412 413
    end

414
    def detailed_status(current_user)
D
Douwe Maan 已提交
415 416 417
      Gitlab::Ci::Status::Pipeline::Factory
        .new(self, current_user)
        .fabricate!
418 419
    end

420 421
    private

422
    def pipeline_data
423
      Gitlab::DataBuilder::Pipeline.build(self)
K
Kamil Trzcinski 已提交
424
    end
425

426
    def latest_builds_status
427 428 429
      return 'failed' unless yaml_errors.blank?

      statuses.latest.status || 'skipped'
K
Kamil Trzcinski 已提交
430
    end
431 432

    def keep_around_commits
433
      return unless project
434

435 436 437
      project.repository.keep_around(self.sha)
      project.repository.keep_around(self.before_sha)
    end
D
Douwe Maan 已提交
438 439
  end
end