pipeline.rb 8.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 92
    def self.latest_successful_for(ref)
      where(ref: ref).order(id: :desc).success.first
93 94
    end

D
Douwe Maan 已提交
95 96 97 98
    def self.truncate_sha(sha)
      sha[0...8]
    end

99
    def self.total_duration
L
Lin Jen-Shin 已提交
100
      where.not(duration: nil).sum(:duration)
101 102
    end

103 104
    def stages_count
      statuses.select(:stage).distinct.count
K
Kamil Trzcinski 已提交
105 106
    end

107
    def stages
K
Kamil Trzcinski 已提交
108 109
      status_sql = statuses.latest.where('stage=sg.stage').status_sql

110 111 112 113
      stages_query = statuses.group('stage').select(:stage)
                       .order('max(stage_idx)')

      stages_with_statuses = CommitStatus.from(stages_query, :sg).
K
Kamil Trzcinski 已提交
114 115 116
        pluck('sg.stage', status_sql)

      stages_with_statuses.map do |stage|
117
        Ci::Stage.new(self, name: stage.first, status: stage.last)
K
Kamil Trzcinski 已提交
118 119 120 121 122 123 124
      end
    end

    def artifacts
      builds.latest.with_artifacts_not_expired
    end

K
Kamil Trzcinski 已提交
125 126
    def project_id
      project.id
K
WIP  
Kamil Trzcinski 已提交
127 128
    end

L
Lin Jen-Shin 已提交
129
    # For now the only user who participates is the user who triggered
130
    def participants(_current_user = nil)
131
      Array(user)
132 133
    end

D
Douwe Maan 已提交
134
    def valid_commit_sha
135
      if self.sha == Gitlab::Git::BLANK_SHA
D
Douwe Maan 已提交
136 137 138 139 140
        self.errors.add(:sha, " cant be 00000000 (branch removal)")
      end
    end

    def git_author_name
141
      commit.try(:author_name)
D
Douwe Maan 已提交
142 143 144
    end

    def git_author_email
145
      commit.try(:author_email)
D
Douwe Maan 已提交
146 147 148
    end

    def git_commit_message
149
      commit.try(:message)
D
Douwe Maan 已提交
150 151
    end

152 153 154 155
    def git_commit_title
      commit.try(:title)
    end

D
Douwe Maan 已提交
156
    def short_sha
157
      Ci::Pipeline.truncate_sha(sha)
D
Douwe Maan 已提交
158 159
    end

160
    def commit
K
Kamil Trzcinski 已提交
161
      @commit ||= project.commit(sha)
D
Douwe Maan 已提交
162 163 164 165
    rescue
      nil
    end

K
Kamil Trzcinski 已提交
166 167 168 169
    def branch?
      !tag?
    end

170 171
    def manual_actions
      builds.latest.manual_actions
172 173
    end

K
Kamil Trzcinski 已提交
174
    def retryable?
175
      builds.latest.failed_or_canceled.any?(&:retryable?)
K
Kamil Trzcinski 已提交
176 177
    end

178
    def cancelable?
179
      statuses.cancelable.any?
180 181
    end

K
Kamil Trzcinski 已提交
182
    def cancel_running
183 184 185 186
      Gitlab::OptimisticLocking.retry_lock(
        statuses.cancelable) do |cancelable|
          cancelable.each(&:cancel)
        end
K
Kamil Trzcinski 已提交
187 188
    end

189
    def retry_failed(user)
190 191 192 193 194
      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
195
        end
K
Kamil Trzcinski 已提交
196 197
    end

198
    def mark_as_processable_after_stage(stage_idx)
199
      builds.skipped.where('stage_idx > ?', stage_idx).find_each(&:process)
200 201
    end

K
Kamil Trzcinski 已提交
202 203 204 205 206 207 208
    def latest?
      return false unless ref
      commit = project.commit(ref)
      return false unless commit
      commit.sha == sha
    end

K
Kamil Trzcinski 已提交
209 210 211 212
    def triggered?
      trigger_requests.any?
    end

K
Kamil Trzcinski 已提交
213 214
    def retried
      @retried ||= (statuses.order(id: :desc) - statuses.latest)
D
Douwe Maan 已提交
215 216 217
    end

    def coverage
218
      coverage_array = statuses.latest.map(&:coverage).compact
K
Kamil Trzcinski 已提交
219 220
      if coverage_array.size >= 1
        '%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
D
Douwe Maan 已提交
221 222 223
      end
    end

224 225 226 227 228 229 230 231
    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 已提交
232
    def has_warnings?
233
      builds.latest.failed_but_allowed.any?
234 235
    end

D
Douwe Maan 已提交
236
    def config_processor
237
      return nil unless ci_yaml_file
238 239 240 241 242
      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
243
        self.yaml_errors = e.message
244 245
        nil
      rescue
246
        self.yaml_errors = 'Undefined error'
247 248
        nil
      end
D
Douwe Maan 已提交
249 250
    end

K
Kamil Trzcinski 已提交
251
    def ci_yaml_file
252 253
      return @ci_yaml_file if defined?(@ci_yaml_file)

254 255 256 257
      @ci_yaml_file ||= begin
        blob = project.repository.blob_at(sha, '.gitlab-ci.yml')
        blob.load_all_data!(project.repository)
        blob.data
258 259
      rescue
        nil
260
      end
K
Kamil Trzcinski 已提交
261 262
    end

K
Kamil Trzcinski 已提交
263 264 265 266
    def environments
      builds.where.not(environment: nil).success.pluck(:environment).uniq
    end

J
James Lopez 已提交
267 268 269 270 271 272 273 274 275 276 277 278 279
    # 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

280 281 282 283
    def notes
      Note.for_commit_id(sha)
    end

284 285 286
    def process!
      Ci::ProcessPipelineService.new(project, user).execute(self)
    end
287

288
    def update_status
K
Kamil Trzcinski 已提交
289
      Gitlab::OptimisticLocking.retry_lock(self) do
K
Kamil Trzcinski 已提交
290
        case latest_builds_status
291 292 293 294 295 296 297
        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
298
      end
299 300
    end

301 302 303 304 305 306
    def predefined_variables
      [
        { key: 'CI_PIPELINE_ID', value: id.to_s, public: true }
      ]
    end

307 308 309 310 311 312 313
    def queued_duration
      return unless started_at

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

314
    def update_duration
315 316
      return unless started_at

317
      self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
318 319 320
    end

    def execute_hooks
K
Kamil Trzcinski 已提交
321 322 323
      data = pipeline_data
      project.execute_hooks(data, :pipeline_hooks)
      project.execute_services(data, :pipeline_hooks)
324 325
    end

326 327 328
    # Merge requests for which the current pipeline is running against
    # the merge request's latest commit.
    def merge_requests
329 330
      @merge_requests ||= project.merge_requests
        .where(source_branch: self.ref)
331
        .select { |merge_request| merge_request.head_pipeline.try(:id) == self.id }
332 333
    end

334 335 336 337
    def detailed_status
      Gitlab::Ci::Status::Pipeline::Factory.new(self).fabricate!
    end

338 339
    private

340
    def pipeline_data
341
      Gitlab::DataBuilder::Pipeline.build(self)
K
Kamil Trzcinski 已提交
342
    end
343

344
    def latest_builds_status
345 346 347
      return 'failed' unless yaml_errors.blank?

      statuses.latest.status || 'skipped'
K
Kamil Trzcinski 已提交
348
    end
349 350

    def keep_around_commits
351
      return unless project
352

353 354 355
      project.repository.keep_around(self.sha)
      project.repository.keep_around(self.before_sha)
    end
D
Douwe Maan 已提交
356 357
  end
end