pipeline.rb 9.6 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?
K
Kamil Trzcinski 已提交
25

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

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

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

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

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

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

52 53 54 55
      event :block do
        transition any - [:blocked] => :blocked
      end

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

257 258 259
    def config_builds_attributes
      return [] unless config_processor

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

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

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

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

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

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

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

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

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

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

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

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

339 340 341 342 343 344 345
    def queued_duration
      return unless started_at

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

346
    def update_duration
347 348
      return unless started_at

349
      self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
350 351 352
    end

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

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

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

372 373
    private

374
    def pipeline_data
375
      Gitlab::DataBuilder::Pipeline.build(self)
K
Kamil Trzcinski 已提交
376
    end
377

378
    def latest_builds_status
379 380 381
      return 'failed' unless yaml_errors.blank?

      statuses.latest.status || 'skipped'
K
Kamil Trzcinski 已提交
382
    end
383 384

    def keep_around_commits
385
      return unless project
386

387 388 389
      project.repository.keep_around(self.sha)
      project.repository.keep_around(self.before_sha)
    end
D
Douwe Maan 已提交
390 391
  end
end