pipeline.rb 8.0 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'

K
Kamil Trzcinski 已提交
10
    belongs_to :project, class_name: '::Project', foreign_key: :gl_project_id
11 12
    belongs_to :user

13 14
    has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
    has_many :builds, class_name: 'Ci::Build', foreign_key: :commit_id
15
    has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest', 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_save :keep_around_commits, unless: :importing?
K
Kamil Trzcinski 已提交
23

24 25
    delegate :stages, to: :statuses

26
    state_machine :status, initial: :created do
27
      event :enqueue do
K
Kamil Trzcinski 已提交
28
        transition created: :pending
29
        transition [:success, :failed, :canceled, :skipped] => :running
30 31 32
      end

      event :run do
33
        transition any => :running
34 35
      end

36 37 38 39 40 41 42 43
      event :skip do
        transition any => :skipped
      end

      event :drop do
        transition any => :failed
      end

44 45 46 47 48 49
      event :succeed do
        transition any => :success
      end

      event :cancel do
        transition any => :canceled
50 51
      end

K
Kamil Trzcinski 已提交
52 53 54 55
      # IMPORTANT
      # Do not add any operations to this state_machine
      # Create a separate worker for each new operation

56
      before_transition [:created, :pending] => :running do |pipeline|
57
        pipeline.started_at = Time.now
58 59
      end

60
      before_transition any => [:success, :failed, :canceled] do |pipeline|
61
        pipeline.finished_at = Time.now
62 63
      end

64 65 66 67
      before_transition do |pipeline|
        pipeline.update_duration
      end

68
      after_transition [:created, :pending] => :running do |pipeline|
K
Kamil Trzcinski 已提交
69
        pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) }
70 71 72
      end

      after_transition any => [:success] do |pipeline|
K
Kamil Trzcinski 已提交
73
        pipeline.run_after_commit { PipelineMetricsWorker.perform_async(id) }
74 75
      end

76
      after_transition [:created, :pending, :running] => :success do |pipeline|
77
        pipeline.run_after_commit { PipelineSuccessWorker.perform_async(id) }
78
      end
79 80

      after_transition do |pipeline, transition|
81 82 83 84 85
        next if transition.loopback?

        pipeline.run_after_commit do
          PipelineHooksWorker.perform_async(id)
        end
86
      end
87 88
    end

89
    # ref can't be HEAD or SHA, can only be branch/tag name
90 91
    def self.latest_successful_for(ref)
      where(ref: ref).order(id: :desc).success.first
92 93
    end

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

K
Kamil Trzcinski 已提交
98
    def self.stages
K
Kamil Trzcinski 已提交
99
      # We use pluck here due to problems with MySQL which doesn't allow LIMIT/OFFSET in queries
100
      CommitStatus.where(pipeline: pluck(:id)).stages
K
Kamil Trzcinski 已提交
101 102
    end

103
    def self.total_duration
L
Lin Jen-Shin 已提交
104
      where.not(duration: nil).sum(:duration)
105 106
    end

107
    def stages_with_latest_statuses
108
      statuses.latest.includes(project: :namespace).order(:stage_idx).group_by(&:stage)
109 110
    end

K
Kamil Trzcinski 已提交
111 112
    def project_id
      project.id
K
WIP  
Kamil Trzcinski 已提交
113 114
    end

D
Douwe Maan 已提交
115
    def valid_commit_sha
116
      if self.sha == Gitlab::Git::BLANK_SHA
D
Douwe Maan 已提交
117 118 119 120 121
        self.errors.add(:sha, " cant be 00000000 (branch removal)")
      end
    end

    def git_author_name
122
      commit.try(:author_name)
D
Douwe Maan 已提交
123 124 125
    end

    def git_author_email
126
      commit.try(:author_email)
D
Douwe Maan 已提交
127 128 129
    end

    def git_commit_message
130
      commit.try(:message)
D
Douwe Maan 已提交
131 132
    end

133 134 135 136
    def git_commit_title
      commit.try(:title)
    end

D
Douwe Maan 已提交
137
    def short_sha
138
      Ci::Pipeline.truncate_sha(sha)
D
Douwe Maan 已提交
139 140
    end

141
    def commit
K
Kamil Trzcinski 已提交
142
      @commit ||= project.commit(sha)
D
Douwe Maan 已提交
143 144 145 146
    rescue
      nil
    end

K
Kamil Trzcinski 已提交
147 148 149 150
    def branch?
      !tag?
    end

151 152
    def manual_actions
      builds.latest.manual_actions
153 154
    end

K
Kamil Trzcinski 已提交
155 156
    def retryable?
      builds.latest.any? do |build|
157
        (build.failed? || build.canceled?) && build.retryable?
K
Kamil Trzcinski 已提交
158 159 160
      end
    end

161 162 163 164
    def cancelable?
      builds.running_or_pending.any?
    end

K
Kamil Trzcinski 已提交
165 166 167 168
    def cancel_running
      builds.running_or_pending.each(&:cancel)
    end

169 170 171 172
    def retry_failed(user)
      builds.latest.failed.select(&:retryable?).each do |build|
        Ci::Build.retry(build, user)
      end
K
Kamil Trzcinski 已提交
173 174
    end

175
    def mark_as_processable_after_stage(stage_idx)
176
      builds.skipped.where('stage_idx > ?', stage_idx).find_each(&:process)
177 178
    end

K
Kamil Trzcinski 已提交
179 180 181 182 183 184 185
    def latest?
      return false unless ref
      commit = project.commit(ref)
      return false unless commit
      commit.sha == sha
    end

K
Kamil Trzcinski 已提交
186 187 188 189
    def triggered?
      trigger_requests.any?
    end

K
Kamil Trzcinski 已提交
190 191
    def retried
      @retried ||= (statuses.order(id: :desc) - statuses.latest)
D
Douwe Maan 已提交
192 193 194
    end

    def coverage
195
      coverage_array = statuses.latest.map(&:coverage).compact
K
Kamil Trzcinski 已提交
196 197
      if coverage_array.size >= 1
        '%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
D
Douwe Maan 已提交
198 199 200
      end
    end

201 202 203 204 205 206 207 208
    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 已提交
209
    def has_warnings?
210
      builds.latest.failed_but_allowed.any?
211 212
    end

D
Douwe Maan 已提交
213
    def config_processor
214
      return nil unless ci_yaml_file
215 216 217 218 219
      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
220
        self.yaml_errors = e.message
221 222
        nil
      rescue
223
        self.yaml_errors = 'Undefined error'
224 225
        nil
      end
D
Douwe Maan 已提交
226 227
    end

K
Kamil Trzcinski 已提交
228
    def ci_yaml_file
229 230
      return @ci_yaml_file if defined?(@ci_yaml_file)

231 232 233 234
      @ci_yaml_file ||= begin
        blob = project.repository.blob_at(sha, '.gitlab-ci.yml')
        blob.load_all_data!(project.repository)
        blob.data
235 236
      rescue
        nil
237
      end
K
Kamil Trzcinski 已提交
238 239
    end

K
Kamil Trzcinski 已提交
240 241 242 243
    def environments
      builds.where.not(environment: nil).success.pluck(:environment).uniq
    end

J
James Lopez 已提交
244 245 246 247 248 249 250 251 252 253 254 255 256
    # 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

257 258 259 260
    def notes
      Note.for_commit_id(sha)
    end

261 262 263
    def process!
      Ci::ProcessPipelineService.new(project, user).execute(self)
    end
264

265
    def update_status
266
      with_lock do
K
Kamil Trzcinski 已提交
267
        case latest_builds_status
268 269 270 271 272 273 274
        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
275
      end
276 277
    end

278 279 280 281 282 283
    def predefined_variables
      [
        { key: 'CI_PIPELINE_ID', value: id.to_s, public: true }
      ]
    end

284 285 286 287 288 289 290
    def queued_duration
      return unless started_at

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

291
    def update_duration
292 293
      return unless started_at

294
      self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
295 296 297
    end

    def execute_hooks
K
Kamil Trzcinski 已提交
298 299 300
      data = pipeline_data
      project.execute_hooks(data, :pipeline_hooks)
      project.execute_services(data, :pipeline_hooks)
301 302
    end

303 304 305
    # Merge requests for which the current pipeline is running against
    # the merge request's latest commit.
    def merge_requests
306 307 308
      @merge_requests ||= project.merge_requests
        .where(source_branch: self.ref)
        .select { |merge_request| merge_request.pipeline.try(:id) == self.id }
309 310
    end

311 312
    private

313
    def pipeline_data
314
      Gitlab::DataBuilder::Pipeline.build(self)
K
Kamil Trzcinski 已提交
315
    end
316

317
    def latest_builds_status
318 319 320
      return 'failed' unless yaml_errors.blank?

      statuses.latest.status || 'skipped'
K
Kamil Trzcinski 已提交
321
    end
322 323

    def keep_around_commits
324
      return unless project
325

326 327 328
      project.repository.keep_around(self.sha)
      project.repository.keep_around(self.before_sha)
    end
D
Douwe Maan 已提交
329 330
  end
end