pipeline.rb 9.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
K
WIP  
Kamil Trzcinski 已提交
7

K
Kamil Trzcinski 已提交
8 9
    self.table_name = 'ci_commits'

10
    belongs_to :project, foreign_key: :gl_project_id
11 12
    belongs_to :user

13
    has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
14 15
    has_many :builds, foreign_key: :commit_id
    has_many :trigger_requests, dependent: :destroy, foreign_key: :commit_id
D
Douwe Maan 已提交
16

D
Douwe Maan 已提交
17 18
    delegate :id, to: :project, prefix: true

D
Douwe Maan 已提交
19 20 21
    validates :sha, presence: { unless: :importing? }
    validates :ref, presence: { unless: :importing? }
    validates :status, presence: { unless: :importing? }
22
    validate :valid_commit_sha, unless: :importing?
D
Douwe Maan 已提交
23

24
    after_create :keep_around_commits, unless: :importing?
25
    after_create :refresh_build_status_cache
K
Kamil Trzcinski 已提交
26

27
    state_machine :status, initial: :created do
28
      event :enqueue do
K
Kamil Trzcinski 已提交
29
        transition created: :pending
30
        transition [:success, :failed, :canceled, :skipped] => :running
31 32 33
      end

      event :run do
K
Kamil Trzcinski 已提交
34
        transition any - [:running] => :running
35 36
      end

37
      event :skip do
K
Kamil Trzcinski 已提交
38
        transition any - [:skipped] => :skipped
39 40 41
      end

      event :drop do
K
Kamil Trzcinski 已提交
42
        transition any - [:failed] => :failed
43 44
      end

45
      event :succeed do
K
Kamil Trzcinski 已提交
46
        transition any - [:success] => :success
47 48 49
      end

      event :cancel do
K
Kamil Trzcinski 已提交
50
        transition any - [:canceled] => :canceled
51 52
      end

53
      event :block do
54
        transition any - [:manual] => :manual
55 56
      end

K
Kamil Trzcinski 已提交
57 58 59 60
      # IMPORTANT
      # Do not add any operations to this state_machine
      # Create a separate worker for each new operation

61
      before_transition [:created, :pending] => :running do |pipeline|
62
        pipeline.started_at = Time.now
63 64
      end

65
      before_transition any => [:success, :failed, :canceled] do |pipeline|
66
        pipeline.finished_at = Time.now
67 68 69
        pipeline.update_duration
      end

70
      after_transition [:created, :pending] => :running do |pipeline|
K
Kamil Trzcinski 已提交
71
        pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) }
72 73 74
      end

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

78
      after_transition [:created, :pending, :running] => :success do |pipeline|
79
        pipeline.run_after_commit { PipelineSuccessWorker.perform_async(id) }
80
      end
81 82

      after_transition do |pipeline, transition|
83 84 85 86 87
        next if transition.loopback?

        pipeline.run_after_commit do
          PipelineHooksWorker.perform_async(id)
        end
88
      end
89

90
      after_transition any => [:success, :failed] do |pipeline|
91
        pipeline.run_after_commit do
92
          PipelineNotificationWorker.perform_async(pipeline.id)
93
        end
94
      end
95 96
    end

97
    # ref can't be HEAD or SHA, can only be branch/tag name
98
    scope :latest, ->(ref = nil) do
D
Douwe Maan 已提交
99 100 101
      max_id = unscope(:select)
        .select("max(#{quoted_table_name}.id)")
        .group(:ref, :sha)
102

103 104 105 106 107
      if ref
        where(ref: ref, id: max_id.where(ref: ref))
      else
        where(id: max_id)
      end
108
    end
109

110 111 112 113
    def self.latest_status(ref = nil)
      latest(ref).status
    end

114
    def self.latest_successful_for(ref)
115
      success.latest(ref).order(id: :desc).first
116 117
    end

D
Douwe Maan 已提交
118 119 120 121
    def self.truncate_sha(sha)
      sha[0...8]
    end

122
    def self.total_duration
L
Lin Jen-Shin 已提交
123
      where.not(duration: nil).sum(:duration)
124 125
    end

K
Kamil Trzcinski 已提交
126 127 128 129 130
    def stage(name)
      stage = Ci::Stage.new(self, name: name)
      stage unless stage.statuses_count.zero?
    end

131 132
    def stages_count
      statuses.select(:stage).distinct.count
K
Kamil Trzcinski 已提交
133 134
    end

K
Kamil Trzcinski 已提交
135
    def stages_name
136 137
      statuses.order(:stage_idx).distinct.
        pluck(:stage, :stage_idx).map(&:first)
K
Kamil Trzcinski 已提交
138 139
    end

140
    def stages
141 142
      # TODO, this needs refactoring, see gitlab-ce#26481.

D
Douwe Maan 已提交
143 144
      stages_query = statuses
        .group('stage').select(:stage).order('max(stage_idx)')
145

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

148
      warnings_sql = statuses.latest.select('COUNT(*)')
D
Douwe Maan 已提交
149
        .where('stage=sg.stage').failed_but_allowed.to_sql
150

D
Douwe Maan 已提交
151 152
      stages_with_statuses = CommitStatus.from(stages_query, :sg)
        .pluck('sg.stage', status_sql, "(#{warnings_sql})")
K
Kamil Trzcinski 已提交
153 154

      stages_with_statuses.map do |stage|
155
        Ci::Stage.new(self, Hash[%i[name status warnings].zip(stage)])
K
Kamil Trzcinski 已提交
156 157 158 159
      end
    end

    def artifacts
160
      builds.latest.with_artifacts_not_expired.includes(project: [:namespace])
K
Kamil Trzcinski 已提交
161 162
    end

L
Lin Jen-Shin 已提交
163
    # For now the only user who participates is the user who triggered
164
    def participants(_current_user = nil)
165
      Array(user)
166 167
    end

D
Douwe Maan 已提交
168
    def valid_commit_sha
169
      if self.sha == Gitlab::Git::BLANK_SHA
D
Douwe Maan 已提交
170 171 172 173 174
        self.errors.add(:sha, " cant be 00000000 (branch removal)")
      end
    end

    def git_author_name
175
      commit.try(:author_name)
D
Douwe Maan 已提交
176 177 178
    end

    def git_author_email
179
      commit.try(:author_email)
D
Douwe Maan 已提交
180 181 182
    end

    def git_commit_message
183
      commit.try(:message)
D
Douwe Maan 已提交
184 185
    end

186 187 188 189
    def git_commit_title
      commit.try(:title)
    end

D
Douwe Maan 已提交
190
    def short_sha
191
      Ci::Pipeline.truncate_sha(sha)
D
Douwe Maan 已提交
192 193
    end

194
    def commit
K
Kamil Trzcinski 已提交
195
      @commit ||= project.commit(sha)
D
Douwe Maan 已提交
196 197 198 199
    rescue
      nil
    end

K
Kamil Trzcinski 已提交
200 201 202 203
    def branch?
      !tag?
    end

204
    def manual_actions
205 206 207 208 209
      builds.latest.manual_actions.includes(project: [:namespace])
    end

    def stuck?
      builds.pending.any?(&:stuck?)
210 211
    end

K
Kamil Trzcinski 已提交
212
    def retryable?
213
      builds.latest.failed_or_canceled.any?(&:retryable?)
K
Kamil Trzcinski 已提交
214 215
    end

216
    def cancelable?
217
      statuses.cancelable.any?
218 219
    end

K
Kamil Trzcinski 已提交
220
    def cancel_running
221 222
      Gitlab::OptimisticLocking.retry_lock(
        statuses.cancelable) do |cancelable|
223
          cancelable.find_each(&:cancel)
224
        end
K
Kamil Trzcinski 已提交
225 226
    end

227
    def retry_failed(current_user)
D
Douwe Maan 已提交
228 229
      Ci::RetryPipelineService.new(project, current_user)
        .execute(self)
K
Kamil Trzcinski 已提交
230 231
    end

232
    def mark_as_processable_after_stage(stage_idx)
233
      builds.skipped.after_stage(stage_idx).find_each(&:process)
234 235
    end

K
Kamil Trzcinski 已提交
236 237 238 239 240 241 242
    def latest?
      return false unless ref
      commit = project.commit(ref)
      return false unless commit
      commit.sha == sha
    end

K
Kamil Trzcinski 已提交
243 244 245 246
    def triggered?
      trigger_requests.any?
    end

K
Kamil Trzcinski 已提交
247 248
    def retried
      @retried ||= (statuses.order(id: :desc) - statuses.latest)
D
Douwe Maan 已提交
249 250 251
    end

    def coverage
252
      coverage_array = statuses.latest.map(&:coverage).compact
K
Kamil Trzcinski 已提交
253 254
      if coverage_array.size >= 1
        '%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
D
Douwe Maan 已提交
255 256 257
      end
    end

258 259 260
    def config_builds_attributes
      return [] unless config_processor

261 262 263
      config_processor.
        builds_for_ref(ref, tag?, trigger_requests.first).
        sort_by { |build| build[:stage_idx] }
264 265
    end

C
Connor Shea 已提交
266
    def has_warnings?
267
      builds.latest.failed_but_allowed.any?
268 269
    end

D
Douwe Maan 已提交
270
    def config_processor
271
      return nil unless ci_yaml_file
272 273 274 275 276
      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
277
        self.yaml_errors = e.message
278 279
        nil
      rescue
280
        self.yaml_errors = 'Undefined error'
281 282
        nil
      end
D
Douwe Maan 已提交
283 284
    end

K
Kamil Trzcinski 已提交
285
    def ci_yaml_file
286 287
      return @ci_yaml_file if defined?(@ci_yaml_file)

D
Douwe Maan 已提交
288
      @ci_yaml_file = project.repository.gitlab_ci_yml_for(sha) rescue nil
K
Kamil Trzcinski 已提交
289 290
    end

291 292 293 294
    def has_yaml_errors?
      yaml_errors.present?
    end

K
Kamil Trzcinski 已提交
295 296 297 298
    def environments
      builds.where.not(environment: nil).success.pluck(:environment).uniq
    end

J
James Lopez 已提交
299 300 301 302 303 304 305 306 307 308 309 310 311
    # 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

312 313 314 315
    def notes
      Note.for_commit_id(sha)
    end

316 317 318
    def process!
      Ci::ProcessPipelineService.new(project, user).execute(self)
    end
319

320
    def update_status
K
Kamil Trzcinski 已提交
321
      Gitlab::OptimisticLocking.retry_lock(self) do
K
Kamil Trzcinski 已提交
322
        case latest_builds_status
323 324 325 326 327 328
        when 'pending' then enqueue
        when 'running' then run
        when 'success' then succeed
        when 'failed' then drop
        when 'canceled' then cancel
        when 'skipped' then skip
329
        when 'manual' then block
330
        end
331
      end
332
      refresh_build_status_cache
333 334
    end

335 336 337 338 339 340
    def predefined_variables
      [
        { key: 'CI_PIPELINE_ID', value: id.to_s, public: true }
      ]
    end

341 342 343 344 345 346 347
    def queued_duration
      return unless started_at

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

348
    def update_duration
349 350
      return unless started_at

351
      self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
352 353 354
    end

    def execute_hooks
K
Kamil Trzcinski 已提交
355 356 357
      data = pipeline_data
      project.execute_hooks(data, :pipeline_hooks)
      project.execute_services(data, :pipeline_hooks)
358 359
    end

360 361 362
    # Merge requests for which the current pipeline is running against
    # the merge request's latest commit.
    def merge_requests
D
Douwe Maan 已提交
363 364 365
      @merge_requests ||= project.merge_requests
        .where(source_branch: self.ref)
        .select { |merge_request| merge_request.head_pipeline.try(:id) == self.id }
366 367
    end

368
    def detailed_status(current_user)
D
Douwe Maan 已提交
369 370 371
      Gitlab::Ci::Status::Pipeline::Factory
        .new(self, current_user)
        .fabricate!
372 373
    end

374 375 376 377
    def refresh_build_status_cache
      Ci::PipelineStatus.new(project, sha: sha, status: status).store_in_cache_if_needed
    end

378 379
    private

380
    def pipeline_data
381
      Gitlab::DataBuilder::Pipeline.build(self)
K
Kamil Trzcinski 已提交
382
    end
383

384
    def latest_builds_status
385 386 387
      return 'failed' unless yaml_errors.blank?

      statuses.latest.status || 'skipped'
K
Kamil Trzcinski 已提交
388
    end
389 390

    def keep_around_commits
391
      return unless project
392

393 394 395
      project.repository.keep_around(self.sha)
      project.repository.keep_around(self.before_sha)
    end
D
Douwe Maan 已提交
396 397
  end
end