pipeline.rb 8.5 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 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
K
Kamil Trzcinski 已提交
33
        transition any - [:running] => :running
34 35
      end

36
      event :skip do
K
Kamil Trzcinski 已提交
37
        transition any - [:skipped] => :skipped
38 39 40
      end

      event :drop do
K
Kamil Trzcinski 已提交
41
        transition any - [:failed] => :failed
42 43
      end

44
      event :succeed do
K
Kamil Trzcinski 已提交
45
        transition any - [:success] => :success
46 47 48
      end

      event :cancel do
K
Kamil Trzcinski 已提交
49
        transition any - [:canceled] => :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 64
        pipeline.update_duration
      end

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

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

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

      after_transition do |pipeline, transition|
78 79 80 81 82
        next if transition.loopback?

        pipeline.run_after_commit do
          PipelineHooksWorker.perform_async(id)
        end
83
      end
84

85
      after_transition any => [:success, :failed] do |pipeline|
86
        pipeline.run_after_commit do
87
          PipelineNotificationWorker.perform_async(pipeline.id)
88
        end
89
      end
90 91
    end

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

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

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

106
    def self.total_duration
L
Lin Jen-Shin 已提交
107
      where.not(duration: nil).sum(:duration)
108 109
    end

110
    def stages_with_latest_statuses
111
      statuses.latest.includes(project: :namespace).order(:stage_idx).group_by(&:stage)
112 113
    end

K
Kamil Trzcinski 已提交
114 115
    def project_id
      project.id
K
WIP  
Kamil Trzcinski 已提交
116 117
    end

L
Lin Jen-Shin 已提交
118
    # For now the only user who participates is the user who triggered
119
    def participants(_current_user = nil)
120
      Array(user)
121 122
    end

D
Douwe Maan 已提交
123
    def valid_commit_sha
124
      if self.sha == Gitlab::Git::BLANK_SHA
D
Douwe Maan 已提交
125 126 127 128 129
        self.errors.add(:sha, " cant be 00000000 (branch removal)")
      end
    end

    def git_author_name
130
      commit.try(:author_name)
D
Douwe Maan 已提交
131 132 133
    end

    def git_author_email
134
      commit.try(:author_email)
D
Douwe Maan 已提交
135 136 137
    end

    def git_commit_message
138
      commit.try(:message)
D
Douwe Maan 已提交
139 140
    end

141 142 143 144
    def git_commit_title
      commit.try(:title)
    end

D
Douwe Maan 已提交
145
    def short_sha
146
      Ci::Pipeline.truncate_sha(sha)
D
Douwe Maan 已提交
147 148
    end

149
    def commit
K
Kamil Trzcinski 已提交
150
      @commit ||= project.commit(sha)
D
Douwe Maan 已提交
151 152 153 154
    rescue
      nil
    end

K
Kamil Trzcinski 已提交
155 156 157 158
    def branch?
      !tag?
    end

159 160
    def manual_actions
      builds.latest.manual_actions
161 162
    end

K
Kamil Trzcinski 已提交
163
    def retryable?
164
      builds.latest.failed_or_canceled.any?(&:retryable?)
K
Kamil Trzcinski 已提交
165 166
    end

167
    def cancelable?
168
      statuses.cancelable.any?
169 170
    end

K
Kamil Trzcinski 已提交
171
    def cancel_running
172 173 174 175
      Gitlab::OptimisticLocking.retry_lock(
        statuses.cancelable) do |cancelable|
          cancelable.each(&:cancel)
        end
K
Kamil Trzcinski 已提交
176 177
    end

178
    def retry_failed(user)
179 180 181 182 183
      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
184
        end
K
Kamil Trzcinski 已提交
185 186
    end

187
    def mark_as_processable_after_stage(stage_idx)
188
      builds.skipped.where('stage_idx > ?', stage_idx).find_each(&:process)
189 190
    end

K
Kamil Trzcinski 已提交
191 192 193 194 195 196 197
    def latest?
      return false unless ref
      commit = project.commit(ref)
      return false unless commit
      commit.sha == sha
    end

K
Kamil Trzcinski 已提交
198 199 200 201
    def triggered?
      trigger_requests.any?
    end

K
Kamil Trzcinski 已提交
202 203
    def retried
      @retried ||= (statuses.order(id: :desc) - statuses.latest)
D
Douwe Maan 已提交
204 205 206
    end

    def coverage
207
      coverage_array = statuses.latest.map(&:coverage).compact
K
Kamil Trzcinski 已提交
208 209
      if coverage_array.size >= 1
        '%.2f' % (coverage_array.reduce(:+) / coverage_array.size)
D
Douwe Maan 已提交
210 211 212
      end
    end

213 214 215 216 217 218 219 220
    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 已提交
221
    def has_warnings?
222
      builds.latest.failed_but_allowed.any?
223 224
    end

D
Douwe Maan 已提交
225
    def config_processor
226
      return nil unless ci_yaml_file
227 228 229 230 231
      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
232
        self.yaml_errors = e.message
233 234
        nil
      rescue
235
        self.yaml_errors = 'Undefined error'
236 237
        nil
      end
D
Douwe Maan 已提交
238 239
    end

K
Kamil Trzcinski 已提交
240
    def ci_yaml_file
241 242
      return @ci_yaml_file if defined?(@ci_yaml_file)

243 244 245 246
      @ci_yaml_file ||= begin
        blob = project.repository.blob_at(sha, '.gitlab-ci.yml')
        blob.load_all_data!(project.repository)
        blob.data
247 248
      rescue
        nil
249
      end
K
Kamil Trzcinski 已提交
250 251
    end

K
Kamil Trzcinski 已提交
252 253 254 255
    def environments
      builds.where.not(environment: nil).success.pluck(:environment).uniq
    end

J
James Lopez 已提交
256 257 258 259 260 261 262 263 264 265 266 267 268
    # 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

269 270 271 272
    def notes
      Note.for_commit_id(sha)
    end

273 274 275
    def process!
      Ci::ProcessPipelineService.new(project, user).execute(self)
    end
276

277
    def update_status
K
Kamil Trzcinski 已提交
278
      Gitlab::OptimisticLocking.retry_lock(self) do
K
Kamil Trzcinski 已提交
279
        case latest_builds_status
280 281 282 283 284 285 286
        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
287
      end
288 289
    end

290 291 292 293 294 295
    def predefined_variables
      [
        { key: 'CI_PIPELINE_ID', value: id.to_s, public: true }
      ]
    end

296 297 298 299 300 301 302
    def queued_duration
      return unless started_at

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

303
    def update_duration
304 305
      return unless started_at

306
      self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
307 308 309
    end

    def execute_hooks
K
Kamil Trzcinski 已提交
310 311 312
      data = pipeline_data
      project.execute_hooks(data, :pipeline_hooks)
      project.execute_services(data, :pipeline_hooks)
313 314
    end

315 316 317
    # Merge requests for which the current pipeline is running against
    # the merge request's latest commit.
    def merge_requests
318 319
      @merge_requests ||= project.merge_requests
        .where(source_branch: self.ref)
320
        .select { |merge_request| merge_request.head_pipeline.try(:id) == self.id }
321 322
    end

323 324
    private

325
    def pipeline_data
326
      Gitlab::DataBuilder::Pipeline.build(self)
K
Kamil Trzcinski 已提交
327
    end
328

329
    def latest_builds_status
330 331 332
      return 'failed' unless yaml_errors.blank?

      statuses.latest.status || 'skipped'
K
Kamil Trzcinski 已提交
333
    end
334 335

    def keep_around_commits
336
      return unless project
337

338 339 340
      project.repository.keep_around(self.sha)
      project.repository.keep_around(self.before_sha)
    end
D
Douwe Maan 已提交
341 342
  end
end