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

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
92 93 94
      max_id = unscope(:select)
        .select("max(#{quoted_table_name}.id)")
        .group(:ref, :sha)
95

96 97
      relation = ref ? where(ref: ref) : self
      relation.where(id: max_id)
98
    end
99

100 101 102 103
    def self.latest_status(ref = nil)
      latest(ref).status
    end

104
    def self.latest_successful_for(ref)
105
      success.latest(ref).order(id: :desc).first
106 107
    end

D
Douwe Maan 已提交
108 109 110 111
    def self.truncate_sha(sha)
      sha[0...8]
    end

112
    def self.total_duration
L
Lin Jen-Shin 已提交
113
      where.not(duration: nil).sum(:duration)
114 115
    end

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

121 122
    def stages_count
      statuses.select(:stage).distinct.count
K
Kamil Trzcinski 已提交
123 124
    end

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

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

      stages_query = statuses
        .group('stage').select(:stage).order('max(stage_idx)')

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

138 139
      warnings_sql = statuses.latest.select('COUNT(*) > 0')
        .where('stage=sg.stage').failed_but_allowed.to_sql
140

141 142
      stages_with_statuses = CommitStatus.from(stages_query, :sg)
        .pluck('sg.stage', status_sql, "(#{warnings_sql})")
K
Kamil Trzcinski 已提交
143 144

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

    def artifacts
150
      builds.latest.with_artifacts_not_expired.includes(project: [:namespace])
K
Kamil Trzcinski 已提交
151 152
    end

K
Kamil Trzcinski 已提交
153 154
    def project_id
      project.id
K
WIP  
Kamil Trzcinski 已提交
155 156
    end

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

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

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

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

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

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

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

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

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

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

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

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

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

K
Kamil Trzcinski 已提交
214
    def cancel_running
215 216
      Gitlab::OptimisticLocking.retry_lock(
        statuses.cancelable) do |cancelable|
217 218 219 220 221 222 223
          cancelable.each do |status|
            if status.created?
              status.skip
            elsif status.active?
              status.cancel
            end
          end
224
        end
K
Kamil Trzcinski 已提交
225 226
    end

227
    def retry_failed(user)
228 229 230 231 232
      Gitlab::OptimisticLocking.retry_lock(
        builds.latest.failed_or_canceled) do |failed_or_canceled|
          failed_or_canceled.select(&:retryable?).each do |build|
            Ci::Build.retry(build, user)
          end
233
        end
K
Kamil Trzcinski 已提交
234 235
    end

236
    def mark_as_processable_after_stage(stage_idx)
237 238 239
      builds.skipped
        .where('stage_idx > ?', stage_idx)
        .find_each(&:process)
240 241
    end

K
Kamil Trzcinski 已提交
242 243 244 245 246 247 248
    def latest?
      return false unless ref
      commit = project.commit(ref)
      return false unless commit
      commit.sha == sha
    end

K
Kamil Trzcinski 已提交
249 250 251 252
    def triggered?
      trigger_requests.any?
    end

K
Kamil Trzcinski 已提交
253 254
    def retried
      @retried ||= (statuses.order(id: :desc) - statuses.latest)
D
Douwe Maan 已提交
255 256 257
    end

    def coverage
258
      coverage_array = statuses.latest.map(&:coverage).compact
K
Kamil Trzcinski 已提交
259 260
      if coverage_array.size >= 1
        '%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
D
Douwe Maan 已提交
261 262 263
      end
    end

264 265 266 267 268 269 270 271
    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 已提交
272
    def has_warnings?
273
      builds.latest.failed_but_allowed.any?
274 275
    end

D
Douwe Maan 已提交
276
    def config_processor
277
      return nil unless ci_yaml_file
278 279 280 281 282
      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
283
        self.yaml_errors = e.message
284 285
        nil
      rescue
286
        self.yaml_errors = 'Undefined error'
287 288
        nil
      end
D
Douwe Maan 已提交
289 290
    end

K
Kamil Trzcinski 已提交
291
    def ci_yaml_file
292 293
      return @ci_yaml_file if defined?(@ci_yaml_file)

D
Douwe Maan 已提交
294
      @ci_yaml_file = project.repository.gitlab_ci_yml_for(sha) rescue nil
K
Kamil Trzcinski 已提交
295 296
    end

297 298 299 300
    def has_yaml_errors?
      yaml_errors.present?
    end

K
Kamil Trzcinski 已提交
301 302 303 304
    def environments
      builds.where.not(environment: nil).success.pluck(:environment).uniq
    end

J
James Lopez 已提交
305 306 307 308 309 310 311 312 313 314 315 316 317
    # 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

318 319 320 321
    def notes
      Note.for_commit_id(sha)
    end

322 323 324
    def process!
      Ci::ProcessPipelineService.new(project, user).execute(self)
    end
325

326
    def update_status
K
Kamil Trzcinski 已提交
327
      Gitlab::OptimisticLocking.retry_lock(self) do
K
Kamil Trzcinski 已提交
328
        case latest_builds_status
329 330 331 332 333 334 335
        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
336
      end
337 338
    end

339 340 341 342 343 344
    def predefined_variables
      [
        { key: 'CI_PIPELINE_ID', value: id.to_s, public: true }
      ]
    end

345 346 347 348 349 350 351
    def queued_duration
      return unless started_at

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

352
    def update_duration
353 354
      return unless started_at

355
      self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
356 357 358
    end

    def execute_hooks
K
Kamil Trzcinski 已提交
359 360 361
      data = pipeline_data
      project.execute_hooks(data, :pipeline_hooks)
      project.execute_services(data, :pipeline_hooks)
362 363
    end

364 365 366
    # Merge requests for which the current pipeline is running against
    # the merge request's latest commit.
    def merge_requests
367 368
      @merge_requests ||= project.merge_requests
        .where(source_branch: self.ref)
369
        .select { |merge_request| merge_request.head_pipeline.try(:id) == self.id }
370 371
    end

372
    def detailed_status(current_user)
373 374 375
      Gitlab::Ci::Status::Pipeline::Factory
        .new(self, current_user)
        .fabricate!
376 377
    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