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

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?
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
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
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.

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

141 142
      warnings_sql = statuses.latest.select('COUNT(*) > 0')
        .where('stage=sg.stage').failed_but_allowed.to_sql
143

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

D
Douwe Maan 已提交
156
    delegate :id, to: :project, prefix: true
K
WIP  
Kamil Trzcinski 已提交
157

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

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

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

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

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

181 182 183 184
    def git_commit_title
      commit.try(:title)
    end

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

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

K
Kamil Trzcinski 已提交
195 196 197 198
    def branch?
      !tag?
    end

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

    def stuck?
      builds.pending.any?(&:stuck?)
205 206
    end

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

211
    def cancelable?
212
      statuses.cancelable.any?
213 214
    end

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

222
    def retry_failed(current_user)
223 224
      Ci::RetryPipelineService.new(project, current_user)
        .execute(self)
K
Kamil Trzcinski 已提交
225 226
    end

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

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

K
Kamil Trzcinski 已提交
238 239 240 241
    def triggered?
      trigger_requests.any?
    end

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

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

253 254 255
    def config_builds_attributes
      return [] unless config_processor

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

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

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

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

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

286 287 288 289
    def has_yaml_errors?
      yaml_errors.present?
    end

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

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

307 308 309 310
    def notes
      Note.for_commit_id(sha)
    end

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

315
    def update_status
K
Kamil Trzcinski 已提交
316
      Gitlab::OptimisticLocking.retry_lock(self) do
K
Kamil Trzcinski 已提交
317
        case latest_builds_status
318 319 320 321 322 323 324
        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
325
      end
326 327
    end

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

334 335 336 337 338 339 340
    def queued_duration
      return unless started_at

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

341
    def update_duration
342 343
      return unless started_at

344
      self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
345 346 347
    end

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

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

361
    def detailed_status(current_user)
362 363 364
      Gitlab::Ci::Status::Pipeline::Factory
        .new(self, current_user)
        .fabricate!
365 366
    end

367 368
    private

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

373
    def latest_builds_status
374 375 376
      return 'failed' unless yaml_errors.blank?

      statuses.latest.status || 'skipped'
K
Kamil Trzcinski 已提交
377
    end
378 379

    def keep_around_commits
380
      return unless project
381

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