pipeline.rb 9.5 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

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

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

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

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

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

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

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

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

K
Kamil Trzcinski 已提交
50 51 52 53
      # IMPORTANT
      # Do not add any operations to this state_machine
      # Create a separate worker for each new operation

54
      before_transition [:created, :pending] => :running do |pipeline|
55
        pipeline.started_at = Time.now
56 57
      end

58
      before_transition any => [:success, :failed, :canceled] do |pipeline|
59
        pipeline.finished_at = Time.now
60 61 62
        pipeline.update_duration
      end

63
      after_transition [:created, :pending] => :running do |pipeline|
K
Kamil Trzcinski 已提交
64
        pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) }
65 66 67
      end

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

71
      after_transition [:created, :pending, :running] => :success do |pipeline|
72
        pipeline.run_after_commit { PipelineSuccessWorker.perform_async(id) }
73
      end
74 75

      after_transition do |pipeline, transition|
76 77 78 79 80
        next if transition.loopback?

        pipeline.run_after_commit do
          PipelineHooksWorker.perform_async(id)
        end
81
      end
82

83
      after_transition any => [:success, :failed] do |pipeline|
84
        pipeline.run_after_commit do
85
          PipelineNotificationWorker.perform_async(pipeline.id)
86
        end
87
      end
88 89
    end

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

96 97 98 99 100
      if ref
        where(ref: ref, id: max_id.where(ref: ref))
      else
        where(id: max_id)
      end
101
    end
102

103 104 105 106
    def self.latest_status(ref = nil)
      latest(ref).status
    end

107
    def self.latest_successful_for(ref)
108
      success.latest(ref).order(id: :desc).first
109 110
    end

D
Douwe Maan 已提交
111 112 113 114
    def self.truncate_sha(sha)
      sha[0...8]
    end

115
    def self.total_duration
L
Lin Jen-Shin 已提交
116
      where.not(duration: nil).sum(:duration)
117 118
    end

K
Kamil Trzcinski 已提交
119 120 121 122 123
    def stage(name)
      stage = Ci::Stage.new(self, name: name)
      stage unless stage.statuses_count.zero?
    end

124 125
    def stages_count
      statuses.select(:stage).distinct.count
K
Kamil Trzcinski 已提交
126 127
    end

K
Kamil Trzcinski 已提交
128
    def stages_name
K
Kamil Trzcinski 已提交
129 130
      statuses.order(:stage_idx).distinct.
        pluck(:stage, :stage_idx).map(&:first)
K
Kamil Trzcinski 已提交
131 132
    end

133
    def stages
134 135
      # TODO, this needs refactoring, see gitlab-ce#26481.

D
Douwe Maan 已提交
136 137
      stages_query = statuses.
        group('stage').select(:stage).order('max(stage_idx)')
138

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

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

D
Douwe Maan 已提交
144 145
      stages_with_statuses = CommitStatus.from(stages_query, :sg).
        pluck('sg.stage', status_sql, "(#{warnings_sql})")
K
Kamil Trzcinski 已提交
146 147

      stages_with_statuses.map do |stage|
148
        Ci::Stage.new(self, Hash[%i[name status warnings].zip(stage)])
K
Kamil Trzcinski 已提交
149 150 151 152
      end
    end

    def artifacts
153
      builds.latest.with_artifacts_not_expired.includes(project: [:namespace])
K
Kamil Trzcinski 已提交
154 155
    end

K
Kamil Trzcinski 已提交
156 157
    def project_id
      project.id
K
WIP  
Kamil Trzcinski 已提交
158 159
    end

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

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

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

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

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

183 184 185 186
    def git_commit_title
      commit.try(:title)
    end

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

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

K
Kamil Trzcinski 已提交
197 198 199 200
    def branch?
      !tag?
    end

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

    def stuck?
      builds.pending.any?(&:stuck?)
207 208
    end

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

213
    def cancelable?
214
      statuses.cancelable.any?
215 216
    end

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

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

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

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

K
Kamil Trzcinski 已提交
240 241 242 243
    def triggered?
      trigger_requests.any?
    end

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

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

255 256 257 258 259 260 261 262
    def config_builds_attributes
      return [] unless config_processor

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

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

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

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

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

288 289 290 291
    def has_yaml_errors?
      yaml_errors.present?
    end

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

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

309 310 311 312
    def notes
      Note.for_commit_id(sha)
    end

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

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

330 331 332 333 334 335
    def predefined_variables
      [
        { key: 'CI_PIPELINE_ID', value: id.to_s, public: true }
      ]
    end

336 337 338 339 340 341 342
    def queued_duration
      return unless started_at

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

343
    def update_duration
344 345
      return unless started_at

346
      self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
347 348 349
    end

    def execute_hooks
K
Kamil Trzcinski 已提交
350 351 352
      data = pipeline_data
      project.execute_hooks(data, :pipeline_hooks)
      project.execute_services(data, :pipeline_hooks)
353 354
    end

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

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

369 370
    private

371
    def pipeline_data
372
      Gitlab::DataBuilder::Pipeline.build(self)
K
Kamil Trzcinski 已提交
373
    end
374

375
    def latest_builds_status
376 377 378
      return 'failed' unless yaml_errors.blank?

      statuses.latest.status || 'skipped'
K
Kamil Trzcinski 已提交
379
    end
380 381

    def keep_around_commits
382
      return unless project
383

384 385 386
      project.repository.keep_around(self.sha)
      project.repository.keep_around(self.before_sha)
    end
D
Douwe Maan 已提交
387 388
  end
end