pipeline.rb 9.46 KB
Newer Older
1
module Ci
2
  class Pipeline < ActiveRecord::Base
3
    extend Ci::Model
4
    include HasStatus
5
    include Importable
6
    include AfterCommitQueue
Kamil Trzcinski's avatar
WIP  
Kamil Trzcinski committed
7

Kamil Trzcinski's avatar
Kamil Trzcinski committed
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
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?
21

22
    after_create :keep_around_commits, unless: :importing?
Kamil Trzcinski's avatar
Kamil Trzcinski committed
23

24
    state_machine :status, initial: :created do
25
      event :enqueue do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
26
        transition created: :pending
27
        transition [:success, :failed, :canceled, :skipped] => :running
28 29 30
      end

      event :run do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
31
        transition any - [:running] => :running
32 33
      end

34
      event :skip do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
35
        transition any - [:skipped] => :skipped
36 37 38
      end

      event :drop do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
39
        transition any - [:failed] => :failed
40 41
      end

42
      event :succeed do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
43
        transition any - [:success] => :success
44 45 46
      end

      event :cancel do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
47
        transition any - [:canceled] => :canceled
48 49
      end

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|
64
        pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) }
65 66 67
      end

      after_transition any => [:success] do |pipeline|
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
      max_id = unscope(:select)
        .select("max(#{quoted_table_name}.id)")
94
        .where(ref: ref)
95
        .group(:ref, :sha)
96

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

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

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

109 110 111 112
    def self.truncate_sha(sha)
      sha[0...8]
    end

113
    def self.total_duration
Lin Jen-Shin's avatar
Lin Jen-Shin committed
114
      where.not(duration: nil).sum(:duration)
115 116
    end

117 118 119 120 121
    def stage(name)
      stage = Ci::Stage.new(self, name: name)
      stage unless stage.statuses_count.zero?
    end

122 123
    def stages_count
      statuses.select(:stage).distinct.count
Kamil Trzcinski's avatar
Kamil Trzcinski committed
124 125
    end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
126
    def stages_name
Kamil Trzcinski's avatar
Kamil Trzcinski committed
127 128
      statuses.order(:stage_idx).distinct.
        pluck(:stage, :stage_idx).map(&:first)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
129 130
    end

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

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

Kamil Trzcinski's avatar
Kamil Trzcinski committed
137 138
      status_sql = statuses.latest.where('stage=sg.stage').status_sql

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

142 143
      stages_with_statuses = CommitStatus.from(stages_query, :sg)
        .pluck('sg.stage', status_sql, "(#{warnings_sql})")
Kamil Trzcinski's avatar
Kamil Trzcinski committed
144 145

      stages_with_statuses.map do |stage|
146
        Ci::Stage.new(self, Hash[%i[name status warnings].zip(stage)])
Kamil Trzcinski's avatar
Kamil Trzcinski committed
147 148 149 150
      end
    end

    def artifacts
151
      builds.latest.with_artifacts_not_expired.includes(project: [:namespace])
Kamil Trzcinski's avatar
Kamil Trzcinski committed
152 153
    end

Kamil Trzcinski's avatar
Kamil Trzcinski committed
154 155
    def project_id
      project.id
Kamil Trzcinski's avatar
WIP  
Kamil Trzcinski committed
156 157
    end

Lin Jen-Shin's avatar
Lin Jen-Shin committed
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

163
    def valid_commit_sha
164
      if self.sha == Gitlab::Git::BLANK_SHA
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)
171 172 173
    end

    def git_author_email
174
      commit.try(:author_email)
175 176 177
    end

    def git_commit_message
178
      commit.try(:message)
179 180
    end

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

185
    def short_sha
186
      Ci::Pipeline.truncate_sha(sha)
187 188
    end

189
    def commit
190
      @commit ||= project.commit(sha)
191 192 193 194
    rescue
      nil
    end

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

207
    def retryable?
208
      builds.latest.failed_or_canceled.any?(&:retryable?)
209 210
    end

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

Kamil Trzcinski's avatar
Kamil Trzcinski committed
215
    def cancel_running
216 217
      Gitlab::OptimisticLocking.retry_lock(
        statuses.cancelable) do |cancelable|
218
          cancelable.find_each(&:cancel)
219
        end
Kamil Trzcinski's avatar
Kamil Trzcinski committed
220 221
    end

222 223 224
    def retry_failed(current_user)
      Ci::RetryPipelineService.new(project, current_user)
        .execute(self)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
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

Kamil Trzcinski's avatar
Kamil Trzcinski committed
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

Kamil Trzcinski's avatar
Kamil Trzcinski committed
238 239 240 241
    def triggered?
      trigger_requests.any?
    end

242 243
    def retried
      @retried ||= (statuses.order(id: :desc) - statuses.latest)
244 245 246
    end

    def coverage
247
      coverage_array = statuses.latest.map(&:coverage).compact
248 249
      if coverage_array.size >= 1
        '%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
250 251 252
      end
    end

253 254 255 256 257 258 259 260
    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

Connor Shea's avatar
Connor Shea committed
261
    def has_warnings?
262
      builds.latest.failed_but_allowed.any?
263 264
    end

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
278 279
    end

280
    def ci_yaml_file
281 282
      return @ci_yaml_file if defined?(@ci_yaml_file)

Douwe Maan's avatar
Douwe Maan committed
283
      @ci_yaml_file = project.repository.gitlab_ci_yml_for(sha) rescue nil
284 285
    end

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

Kamil Trzcinski's avatar
Kamil Trzcinski committed
290 291 292 293
    def environments
      builds.where.not(environment: nil).success.pluck(:environment).uniq
    end

James Lopez's avatar
James Lopez committed
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
316
      Gitlab::OptimisticLocking.retry_lock(self) do
Kamil Trzcinski's avatar
Kamil Trzcinski committed
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
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
      @merge_requests ||= project.merge_requests
        .where(source_branch: self.ref)
358
        .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)
Kamil Trzcinski's avatar
Kamil Trzcinski committed
371
    end
372

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

      statuses.latest.status || 'skipped'
Kamil Trzcinski's avatar
Kamil Trzcinski committed
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
385 386
  end
end