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 Trzciński 已提交
8
    belongs_to :project
9 10
    belongs_to :user

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

D
Douwe Maan 已提交
15 16
    delegate :id, to: :project, prefix: true

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

22
    after_create :keep_around_commits, unless: :importing?
23
    after_create :refresh_build_status_cache
K
Kamil Trzcinski 已提交
24

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

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

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

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

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

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

51
      event :block do
52
        transition any - [:manual] => :manual
53 54
      end

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

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

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

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

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

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

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

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

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

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

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

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

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

116 117 118 119 120 121
    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 已提交
122 123 124 125
    def self.truncate_sha(sha)
      sha[0...8]
    end

126
    def self.total_duration
L
Lin Jen-Shin 已提交
127
      where.not(duration: nil).sum(:duration)
128 129
    end

K
Kamil Trzcinski 已提交
130 131 132 133 134
    def stage(name)
      stage = Ci::Stage.new(self, name: name)
      stage unless stage.statuses_count.zero?
    end

135 136
    def stages_count
      statuses.select(:stage).distinct.count
K
Kamil Trzcinski 已提交
137 138
    end

K
Kamil Trzcinski 已提交
139
    def stages_name
140 141
      statuses.order(:stage_idx).distinct.
        pluck(:stage, :stage_idx).map(&:first)
K
Kamil Trzcinski 已提交
142 143
    end

144
    def stages
145 146
      # TODO, this needs refactoring, see gitlab-ce#26481.

D
Douwe Maan 已提交
147 148
      stages_query = statuses
        .group('stage').select(:stage).order('max(stage_idx)')
149

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

152
      warnings_sql = statuses.latest.select('COUNT(*)')
D
Douwe Maan 已提交
153
        .where('stage=sg.stage').failed_but_allowed.to_sql
154

D
Douwe Maan 已提交
155 156
      stages_with_statuses = CommitStatus.from(stages_query, :sg)
        .pluck('sg.stage', status_sql, "(#{warnings_sql})")
K
Kamil Trzcinski 已提交
157 158

      stages_with_statuses.map do |stage|
159
        Ci::Stage.new(self, Hash[%i[name status warnings].zip(stage)])
K
Kamil Trzcinski 已提交
160 161 162 163
      end
    end

    def artifacts
164
      builds.latest.with_artifacts_not_expired.includes(project: [:namespace])
K
Kamil Trzcinski 已提交
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
      builds.latest.manual_actions.includes(project: [:namespace])
    end

    def stuck?
208
      builds.pending.includes(:project).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 'manual' then block
329
        end
330
      end
331
      refresh_build_status_cache
332 333
    end

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

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

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

347
    def update_duration
348 349
      return unless started_at

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

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

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

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

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

377 378
    private

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

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

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

    def keep_around_commits
390
      return unless project
391

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