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

K
Kamil Trzcinski 已提交
107
    def stages_name
K
Kamil Trzcinski 已提交
108 109
      statuses.order(:stage_idx).distinct.
        pluck(:stage, :stage_idx).map(&:first)
K
Kamil Trzcinski 已提交
110 111
    end

112
    def stages
K
Kamil Trzcinski 已提交
113 114
      status_sql = statuses.latest.where('stage=sg.stage').status_sql

115 116 117 118
      stages_query = statuses.group('stage').select(:stage)
                       .order('max(stage_idx)')

      stages_with_statuses = CommitStatus.from(stages_query, :sg).
K
Kamil Trzcinski 已提交
119 120 121
        pluck('sg.stage', status_sql)

      stages_with_statuses.map do |stage|
122
        Ci::Stage.new(self, name: stage.first, status: stage.last)
K
Kamil Trzcinski 已提交
123 124 125 126 127 128 129
      end
    end

    def artifacts
      builds.latest.with_artifacts_not_expired
    end

K
Kamil Trzcinski 已提交
130 131
    def project_id
      project.id
K
WIP  
Kamil Trzcinski 已提交
132 133
    end

L
Lin Jen-Shin 已提交
134
    # For now the only user who participates is the user who triggered
135
    def participants(_current_user = nil)
136
      Array(user)
137 138
    end

D
Douwe Maan 已提交
139
    def valid_commit_sha
140
      if self.sha == Gitlab::Git::BLANK_SHA
D
Douwe Maan 已提交
141 142 143 144 145
        self.errors.add(:sha, " cant be 00000000 (branch removal)")
      end
    end

    def git_author_name
146
      commit.try(:author_name)
D
Douwe Maan 已提交
147 148 149
    end

    def git_author_email
150
      commit.try(:author_email)
D
Douwe Maan 已提交
151 152 153
    end

    def git_commit_message
154
      commit.try(:message)
D
Douwe Maan 已提交
155 156
    end

157 158 159 160
    def git_commit_title
      commit.try(:title)
    end

D
Douwe Maan 已提交
161
    def short_sha
162
      Ci::Pipeline.truncate_sha(sha)
D
Douwe Maan 已提交
163 164
    end

165
    def commit
K
Kamil Trzcinski 已提交
166
      @commit ||= project.commit(sha)
D
Douwe Maan 已提交
167 168 169 170
    rescue
      nil
    end

K
Kamil Trzcinski 已提交
171 172 173 174
    def branch?
      !tag?
    end

175 176
    def manual_actions
      builds.latest.manual_actions
177 178
    end

K
Kamil Trzcinski 已提交
179
    def retryable?
180
      builds.latest.failed_or_canceled.any?(&:retryable?)
K
Kamil Trzcinski 已提交
181 182
    end

183
    def cancelable?
184
      statuses.cancelable.any?
185 186
    end

K
Kamil Trzcinski 已提交
187
    def cancel_running
188 189 190 191
      Gitlab::OptimisticLocking.retry_lock(
        statuses.cancelable) do |cancelable|
          cancelable.each(&:cancel)
        end
K
Kamil Trzcinski 已提交
192 193
    end

194
    def retry_failed(user)
195 196 197 198 199
      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
200
        end
K
Kamil Trzcinski 已提交
201 202
    end

203
    def mark_as_processable_after_stage(stage_idx)
204
      builds.skipped.where('stage_idx > ?', stage_idx).find_each(&:process)
205 206
    end

K
Kamil Trzcinski 已提交
207 208 209 210 211 212 213
    def latest?
      return false unless ref
      commit = project.commit(ref)
      return false unless commit
      commit.sha == sha
    end

K
Kamil Trzcinski 已提交
214 215 216 217
    def triggered?
      trigger_requests.any?
    end

K
Kamil Trzcinski 已提交
218 219
    def retried
      @retried ||= (statuses.order(id: :desc) - statuses.latest)
D
Douwe Maan 已提交
220 221 222
    end

    def coverage
223
      coverage_array = statuses.latest.map(&:coverage).compact
K
Kamil Trzcinski 已提交
224 225
      if coverage_array.size >= 1
        '%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
D
Douwe Maan 已提交
226 227 228
      end
    end

229 230 231 232 233 234 235 236
    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 已提交
237
    def has_warnings?
238
      builds.latest.failed_but_allowed.any?
239 240
    end

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

K
Kamil Trzcinski 已提交
256
    def ci_yaml_file
257 258
      return @ci_yaml_file if defined?(@ci_yaml_file)

259 260 261 262
      @ci_yaml_file ||= begin
        blob = project.repository.blob_at(sha, '.gitlab-ci.yml')
        blob.load_all_data!(project.repository)
        blob.data
263 264
      rescue
        nil
265
      end
K
Kamil Trzcinski 已提交
266 267
    end

K
Kamil Trzcinski 已提交
268 269 270 271
    def environments
      builds.where.not(environment: nil).success.pluck(:environment).uniq
    end

J
James Lopez 已提交
272 273 274 275 276 277 278 279 280 281 282 283 284
    # 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

285 286 287 288
    def notes
      Note.for_commit_id(sha)
    end

289 290 291
    def process!
      Ci::ProcessPipelineService.new(project, user).execute(self)
    end
292

293
    def update_status
K
Kamil Trzcinski 已提交
294
      Gitlab::OptimisticLocking.retry_lock(self) do
K
Kamil Trzcinski 已提交
295
        case latest_builds_status
296 297 298 299 300 301 302
        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
303
      end
304 305
    end

306 307 308 309 310 311
    def predefined_variables
      [
        { key: 'CI_PIPELINE_ID', value: id.to_s, public: true }
      ]
    end

312 313 314 315 316 317 318
    def queued_duration
      return unless started_at

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

319
    def update_duration
320 321
      return unless started_at

322
      self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
323 324 325
    end

    def execute_hooks
K
Kamil Trzcinski 已提交
326 327 328
      data = pipeline_data
      project.execute_hooks(data, :pipeline_hooks)
      project.execute_services(data, :pipeline_hooks)
329 330
    end

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

339
    def detailed_status(current_user)
340 341 342
      Gitlab::Ci::Status::Pipeline::Factory
        .new(self, current_user)
        .fabricate!
343 344
    end

345 346
    private

347
    def pipeline_data
348
      Gitlab::DataBuilder::Pipeline.build(self)
K
Kamil Trzcinski 已提交
349
    end
350

351
    def latest_builds_status
352 353 354
      return 'failed' unless yaml_errors.blank?

      statuses.latest.status || 'skipped'
K
Kamil Trzcinski 已提交
355
    end
356 357

    def keep_around_commits
358
      return unless project
359

360 361 362
      project.repository.keep_around(self.sha)
      project.repository.keep_around(self.before_sha)
    end
D
Douwe Maan 已提交
363 364
  end
end